IT story

왜이 루프가“경고 : 반복 3u가 정의되지 않은 동작을 불러옵니다”를 생성하고 4 줄 이상을 출력합니까?

hot-time 2020. 6. 5. 08:22
반응형

왜이 루프가“경고 : 반복 3u가 정의되지 않은 동작을 불러옵니다”를 생성하고 4 줄 이상을 출력합니까?


이것을 컴파일 :

#include <iostream>

int main()
{
    for (int i = 0; i < 4; ++i)
        std::cout << i*1000000000 << std::endl;
}

그리고 gcc다음과 같은 경고를 생성합니다 :

warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
   std::cout << i*1000000000 << std::endl;
                  ^

부호있는 정수 오버플로가 있음을 이해합니다.

내가 얻을 수없는 것은 왜 i오버플로 작업으로 인해 값이 깨지는 것입니까?

GCC를 사용하여 x86에서 정수 오버플로가 무한 루프를 일으키는 이유에 대한 답변을 읽었습니다 . 그러나 이런 일이 발생 하는지 아직 확실하지 않습니다. "정의되지 않은"은 "모든 일이 발생할 수 있음"을 의미하지만 이 특정 행동 의 근본 원인은 무엇입니까?

온라인 : http://ideone.com/dMrRKR

컴파일러: gcc (4.8)


부호있는 정수 오버플로 (엄격히 말해서 "부호없는 정수 오버플로"와 같은 것은 없음)는 정의되지 않은 동작을 의미 합니다. 그리고 이것은 모든 일이 일어날 수 있다는 것을 의미하며, C ++의 규칙에 따라 왜 그런 일이 발생하지 않는지 논의하는 것은 의미가 없습니다.

C ++ 11 초안 N3337 : §5.4 : 1

표현식을 평가하는 동안 결과가 수학적으로 정의되지 않았거나 해당 유형의 표현 가능한 값 범위에없는 경우 동작이 정의되지 않습니다. [참고 : 대부분의 기존 C ++ 구현에서는 정수 오버플로를 무시합니다. 0으로 나누기, 제로 제수를 사용하여 나머지 형성 및 모든 부동 소수점 예외는 기계마다 다르며 일반적으로 라이브러리 기능에 의해 조정됩니다. — 끝 노트]

g++ -O3경고로 컴파일 된 코드는 (없이도 -Wall)

a.cpp: In function 'int main()':
a.cpp:11:18: warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
   std::cout << i*1000000000 << std::endl;
                  ^
a.cpp:9:2: note: containing loop
  for (int i = 0; i < 4; ++i)
  ^

프로그램이 수행하는 작업을 분석 할 수있는 유일한 방법은 생성 된 어셈블리 코드를 읽는 것입니다.

전체 어셈블리 목록은 다음과 같습니다.

    .file   "a.cpp"
    .section    .text$_ZNKSt5ctypeIcE8do_widenEc,"x"
    .linkonce discard
    .align 2
LCOLDB0:
LHOTB0:
    .align 2
    .p2align 4,,15
    .globl  __ZNKSt5ctypeIcE8do_widenEc
    .def    __ZNKSt5ctypeIcE8do_widenEc;    .scl    2;  .type   32; .endef
__ZNKSt5ctypeIcE8do_widenEc:
LFB860:
    .cfi_startproc
    movzbl  4(%esp), %eax
    ret $4
    .cfi_endproc
LFE860:
LCOLDE0:
LHOTE0:
    .section    .text.unlikely,"x"
LCOLDB1:
    .text
LHOTB1:
    .p2align 4,,15
    .def    ___tcf_0;   .scl    3;  .type   32; .endef
___tcf_0:
LFB1091:
    .cfi_startproc
    movl    $__ZStL8__ioinit, %ecx
    jmp __ZNSt8ios_base4InitD1Ev
    .cfi_endproc
LFE1091:
    .section    .text.unlikely,"x"
LCOLDE1:
    .text
LHOTE1:
    .def    ___main;    .scl    2;  .type   32; .endef
    .section    .text.unlikely,"x"
LCOLDB2:
    .section    .text.startup,"x"
LHOTB2:
    .p2align 4,,15
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB1084:
    .cfi_startproc
    leal    4(%esp), %ecx
    .cfi_def_cfa 1, 0
    andl    $-16, %esp
    pushl   -4(%ecx)
    pushl   %ebp
    .cfi_escape 0x10,0x5,0x2,0x75,0
    movl    %esp, %ebp
    pushl   %edi
    pushl   %esi
    pushl   %ebx
    pushl   %ecx
    .cfi_escape 0xf,0x3,0x75,0x70,0x6
    .cfi_escape 0x10,0x7,0x2,0x75,0x7c
    .cfi_escape 0x10,0x6,0x2,0x75,0x78
    .cfi_escape 0x10,0x3,0x2,0x75,0x74
    xorl    %edi, %edi
    subl    $24, %esp
    call    ___main
L4:
    movl    %edi, (%esp)
    movl    $__ZSt4cout, %ecx
    call    __ZNSolsEi
    movl    %eax, %esi
    movl    (%eax), %eax
    subl    $4, %esp
    movl    -12(%eax), %eax
    movl    124(%esi,%eax), %ebx
    testl   %ebx, %ebx
    je  L15
    cmpb    $0, 28(%ebx)
    je  L5
    movsbl  39(%ebx), %eax
L6:
    movl    %esi, %ecx
    movl    %eax, (%esp)
    addl    $1000000000, %edi
    call    __ZNSo3putEc
    subl    $4, %esp
    movl    %eax, %ecx
    call    __ZNSo5flushEv
    jmp L4
    .p2align 4,,10
L5:
    movl    %ebx, %ecx
    call    __ZNKSt5ctypeIcE13_M_widen_initEv
    movl    (%ebx), %eax
    movl    24(%eax), %edx
    movl    $10, %eax
    cmpl    $__ZNKSt5ctypeIcE8do_widenEc, %edx
    je  L6
    movl    $10, (%esp)
    movl    %ebx, %ecx
    call    *%edx
    movsbl  %al, %eax
    pushl   %edx
    jmp L6
L15:
    call    __ZSt16__throw_bad_castv
    .cfi_endproc
LFE1084:
    .section    .text.unlikely,"x"
LCOLDE2:
    .section    .text.startup,"x"
LHOTE2:
    .section    .text.unlikely,"x"
LCOLDB3:
    .section    .text.startup,"x"
LHOTB3:
    .p2align 4,,15
    .def    __GLOBAL__sub_I_main;   .scl    3;  .type   32; .endef
__GLOBAL__sub_I_main:
LFB1092:
    .cfi_startproc
    subl    $28, %esp
    .cfi_def_cfa_offset 32
    movl    $__ZStL8__ioinit, %ecx
    call    __ZNSt8ios_base4InitC1Ev
    movl    $___tcf_0, (%esp)
    call    _atexit
    addl    $28, %esp
    .cfi_def_cfa_offset 4
    ret
    .cfi_endproc
LFE1092:
    .section    .text.unlikely,"x"
LCOLDE3:
    .section    .text.startup,"x"
LHOTE3:
    .section    .ctors,"w"
    .align 4
    .long   __GLOBAL__sub_I_main
.lcomm __ZStL8__ioinit,1,1
    .ident  "GCC: (i686-posix-dwarf-rev1, Built by MinGW-W64 project) 4.9.0"
    .def    __ZNSt8ios_base4InitD1Ev;   .scl    2;  .type   32; .endef
    .def    __ZNSolsEi; .scl    2;  .type   32; .endef
    .def    __ZNSo3putEc;   .scl    2;  .type   32; .endef
    .def    __ZNSo5flushEv; .scl    2;  .type   32; .endef
    .def    __ZNKSt5ctypeIcE13_M_widen_initEv;  .scl    2;  .type   32; .endef
    .def    __ZSt16__throw_bad_castv;   .scl    2;  .type   32; .endef
    .def    __ZNSt8ios_base4InitC1Ev;   .scl    2;  .type   32; .endef
    .def    _atexit;    .scl    2;  .type   32; .endef

간신히 어셈블리를 읽을 수는 있지만 addl $1000000000, %edi줄을 볼 수도 있습니다. 결과 코드는 다음과 같습니다

for(int i = 0; /* nothing, that is - infinite loop */; i += 1000000000)
    std::cout << i << std::endl;

@TC의 의견 :

(1) i2보다 큰 값을 가진 모든 반복 에는 정의되지 않은 동작이 있기 때문에 -2 (2) i <= 2최적화 목적으로-> (3) 루프 조건이 항상 참 이라고 가정 할 수 있습니다 -> (4 ) 무한 루프로 최적화되었습니다.

OP 코드의 어셈블리 코드를 정의되지 않은 동작없이 다음 코드의 어셈블리 코드와 비교하는 아이디어를 얻었습니다.

#include <iostream>

int main()
{
    // changed the termination condition
    for (int i = 0; i < 3; ++i)
        std::cout << i*1000000000 << std::endl;
}

실제로 올바른 코드에는 종료 조건이 있습니다.

    ; ...snip...
L6:
    mov ecx, edi
    mov DWORD PTR [esp], eax
    add esi, 1000000000
    call    __ZNSo3putEc
    sub esp, 4
    mov ecx, eax
    call    __ZNSo5flushEv
    cmp esi, -1294967296 // here it is
    jne L7
    lea esp, [ebp-16]
    xor eax, eax
    pop ecx
    ; ...snip...

세상에, 그것은 명확하지 않다! 공평하지 않다! 나는 불의 재판을 요구한다!

그것으로 처리, 당신은 버그가있는 코드를 작성하고 기분이 좋지 않습니다. 결과를 참는다.

... 또는 대안으로 더 나은 진단 및 더 나은 디버깅 도구를 적절하게 사용하십시오.

  • 모든 경고를 활성화

    • -Wall잘못된 경고없이 모든 유용한 경고를 활성화하는 gcc 옵션입니다. 이것은 항상 사용해야하는 최소한의 것입니다.
    • gcc에는 다른 많은 경고 옵션 이 있지만, -Wall오 탐지에 대해 경고 할 수 있으므로 활성화되어 있지 않습니다.
    • 불행히도 Visual C ++는 유용한 경고를 제공 할 수있는 능력이 뒤떨어져 있습니다. 최소한 IDE는 기본적으로 일부를 활성화합니다.
  • 디버깅을 위해 디버그 플래그 사용

    • 정수 오버플로의 경우 오버플로 -ftrapv에서 프로그램을 트랩합니다.
    • 연타 컴파일러는이를위한 우수 : -fcatch-undefined-behavior정의되지 않은 동작의 인스턴스의 많은을 잡는다 (참고 : "a lot of" != "all of them")

나는 내일 배송이 필요한 프로그램이 아닌 스파게티 엉망이있다! 도와주세요 !!!!!!

gcc 사용 -fwrapv

이 옵션은 덧셈, 뺄셈 및 곱셈의 부호있는 산술 오버플로가 2의 보수 표현을 사용한다고 가정하도록 컴파일러에 지시합니다.

1- 이 규칙은 §3.9.1.4에서 말하는 "부호없는 정수 오버플로"에는 적용되지 않습니다

부호없는 것으로 선언 된 부호없는 정수는 산술 모듈로 2n 의 법칙을 준수해야합니다. 여기서 n은 특정 크기의 정수 값을 나타내는 비트 수입니다.

예를 들어, 결과 UINT_MAX + 1는 수학적으로 정의됩니다-산술 모듈로의 규칙 2 n


짧은 대답, 특히이 gcc문제를 문서화 했으므로 gcc 4.8 릴리스 노트에서 다음강조합니다 .

GCC는 이제보다 적극적인 분석을 사용하여 언어 표준에 의해 적용된 제약 조건을 사용하여 루프 반복 횟수의 상한을 도출합니다 . 이로 인해 SPEC CPU 2006 464.h264ref 및 416.gamess와 같은 부적합한 프로그램이 더 이상 예상대로 작동하지 않을 수 있습니다. 이 적극적인 분석을 비활성화하기 위해 새로운 옵션 인 -fno-aggressive-loop-optimizations가 추가되었습니다. 일정한 반복 횟수를 알고 있지만 마지막 반복에 도달하기 전에 또는 마지막 반복 중에 루프에서 정의되지 않은 동작이 발생하는 것으로 알려진 일부 루프에서 GCC는 반복 횟수의 하한을 도출하는 대신 루프에서 정의되지 않은 동작에 대해 경고합니다. 루프. 경고는 -Wno-aggressive-loop-optimizations로 비활성화 할 수 있습니다.

실제로 -fno-aggressive-loop-optimizations무한 루프 동작을 사용 하면 중단해야하며 테스트 한 모든 경우에 적용됩니다.

아는와 긴 대답이 시작 부호있는 정수 오버 플로우가 초안 C ++ 표준 섹션보고에 의해 정의되지 않은 동작입니다 5 표현식4 말한다 :

표현식을 평가하는 동안 결과가 수학적으로 정의되지 않았거나 해당 유형의 표현 가능한 값 범위에없는 경우 동작이 정의되지 않습니다 . [참고 : 대부분의 기존 C ++ 구현에서는 정수 오버플로를 무시합니다. 0으로 나누기, 제로 제수를 사용하여 나머지를 형성하며 모든 부동 소수점 예외는 기계마다 다르며 일반적으로 라이브러리 기능으로 조정할 수 있습니다. — 끝 노트

우리는 표준에 따르면 정의되지 않은 동작은 다음과 같은 정의와 함께 제공되는 메모에서 예측할 수 없다고 말합니다.

[참고 :이 국제 표준이 명시적인 행동 정의를 생략하거나 프로그램이 잘못된 구성 또는 잘못된 데이터를 사용하는 경우 정의되지 않은 동작이 예상 될 수 있습니다. 허용되지 않는 정의 된 동작은 예측할 수없는 결과로 상황을 완전히 무시하는 것부터, 환경의 문서화 된 방식 (진단 메시지 발행 여부에 관계없이)으로 번역 또는 프로그램 실행 중 행동, 번역 또는 실행 종료 (발급 포함)에 이르기까지 다양 합니다. 진단 메시지). 많은 잘못된 프로그램 구성은 정의되지 않은 동작을 유발하지 않습니다. 그들은 진단을 받아야합니다. — 끝 노트]

그러나 gcc옵티마이 저가 이것을 무한 루프로 바꾸기 위해 무엇을 할 수 있을까요? 완전히 엉뚱한 소리. 그러나 고맙게도 gcc우리는 경고에서 그것을 알아낼 수있는 단서를 제공합니다.

warning: iteration 3u invokes undefined behavior [-Waggressive-loop-optimizations]
   std::cout << i*1000000000 << std::endl;
                  ^

실마리는 Waggressive-loop-optimizations무엇입니까? 다행스럽게도이 최적화가 이런 방식으로 코드를 깨뜨린 것은 이번이 처음이 아니며 John Regehr다음 코드를 보여주는 GCC pre-4.8 Breaks Broken SPEC 2006 Benchmarks 기사에 사례를 문서화 했기 때문에 운이 좋았습니다 .

int d[16];

int SATD (void)
{
  int satd = 0, dd, k;
  for (dd=d[k=0]; k<16; dd=d[++k]) {
    satd += (dd < 0 ? -dd : dd);
  }
  return satd;
}

기사는 말합니다 :

정의되지 않은 동작은 루프를 종료하기 직전에 d [16]에 액세스하고 있습니다. C99에서는 배열 끝을지나 한 위치에있는 요소에 대한 포인터를 만드는 것이 합법적이지만 해당 포인터를 역 참조해서는 안됩니다.

그리고 나중에 말합니다 :

자세하게는 다음과 같습니다. d [++ k]를 볼 때 AC 컴파일러는 k의 증가 된 값이 배열 범위 내에 있다고 가정 할 수 있습니다. 그렇지 않으면 정의되지 않은 동작이 발생하기 때문입니다. 이 코드의 경우 GCC는 k가 0..15 범위에 있다고 추론 할 수 있습니다. 조금 후에, GCC가 k <16을 볼 때, "Aha – 그 표현은 항상 참이므로 무한 루프가 있습니다." 컴파일러가 잘 정의 된 가정을 사용하여 유용한 데이터 흐름 사실을 유추하는 상황

따라서 어떤 경우 컴파일러가 해야하는 일은 부호있는 정수 오버플로가 정의되지 않은 동작 이므로 i항상보다 작아야 4하므로 무한 루프가 있어야 한다고 가정 합니다 .

그는 이것이이 코드를 볼 때 악명 높은 Linux 커널 널 포인터 검사 제거 와 매우 유사하다고 설명합니다 .

struct foo *s = ...;
int x = s->f;
if (!s) return ERROR;

gcc널 포인터 s에서 지연된 이후 s->f;널 포인터를 역 참조하는 것은 정의되지 않은 동작 s이므로 널이 아니어야하므로 if (!s)다음 행 에서 확인을 최적화 합니다.

여기서의 교훈은 최신 옵티마이 저가 정의되지 않은 동작을 악용하는 데 매우 공격적이며 대부분 더 공격적이라는 것입니다. 분명히 몇 가지 예만 있으면 최적화 프로그램이 프로그래머에게는 완전히 비합리적으로 보이지만 최적화 프로그램의 관점에서 회상하는 것이 합리적이라는 것을 알 수 있습니다.


tl; dr 이 코드는 integer + positive integer == negative integer 테스트를 생성합니다 . 일반적으로 옵티마이 저는이를 최적화하지 않지만 std::endl다음에 사용되는 특정 경우 컴파일러는이 테스트를 최적화합니다. endl아직 특별한 점을 찾지 못했습니다.


-O1 이상의 어셈블리 코드에서 gcc는 루프를 다음과 같이 리팩터링합니다.

i = 0;
do {
    cout << i << endl;
    i += NUMBER;
} 
while (i != NUMBER * 4)

올바르게 작동하는 가장 큰 값은 715827882즉 floor ( INT_MAX/3)입니다. 의 어셈블리 스 니펫 -O1은 다음과 같습니다.

L4:
movsbl  %al, %eax
movl    %eax, 4(%esp)
movl    $__ZSt4cout, (%esp)
call    __ZNSo3putEc
movl    %eax, (%esp)
call    __ZNSo5flushEv
addl    $715827882, %esi
cmpl    $-1431655768, %esi
jne L6
    // fallthrough to "return" code

은 참고가 -1431655768있다 4 * 7158278822의 보수에.

타격 -O2하면 다음과 같이 최적화됩니다.

L4:
movsbl  %al, %eax
addl    $715827882, %esi
movl    %eax, 4(%esp)
movl    $__ZSt4cout, (%esp)
call    __ZNSo3putEc
movl    %eax, (%esp)
call    __ZNSo5flushEv
cmpl    $-1431655768, %esi
jne L6
leal    -8(%ebp), %esp
jne L6 
   // fallthrough to "return" code

따라서 최적화는 단지 addl위로 올라간 것입니다.

If we recompile with 715827883 instead then the -O1 version is identical apart from the changed number and test value. However, -O2 then makes a change:

L4:
movsbl  %al, %eax
addl    $715827883, %esi
movl    %eax, 4(%esp)
movl    $__ZSt4cout, (%esp)
call    __ZNSo3putEc
movl    %eax, (%esp)
call    __ZNSo5flushEv
jmp L2

Where there was cmpl $-1431655764, %esi at -O1, that line has been removed for -O2. The optimizer must have decided that adding 715827883 to %esi can never equal -1431655764.

This is pretty puzzling. Adding that to INT_MIN+1 does generate the expected result, so the optimizer must have decided that %esi can never be INT_MIN+1 and I'm not sure why it would decide that.

In the working example it seems it'd be equally valid to conclude that adding 715827882 to a number cannot equal INT_MIN + 715827882 - 2 ! (this is only possible if wraparound does actually occur), yet it does not optimize the line out in that example.


The code I was using is:

#include <iostream>
#include <cstdio>

int main()
{
    for (int i = 0; i < 4; ++i)
    {
        //volatile int j = i*715827883;
        volatile int j = i*715827882;
        printf("%d\n", j);

        std::endl(std::cout);
    }
}

If the std::endl(std::cout) is removed then the optimization no longer occurs. In fact replacing it with std::cout.put('\n'); std::flush(std::cout); also causes the optimization to not happen, even though std::endl is inlined.

The inlining of std::endl seems to affect the earlier part of the loop structure (which I don't quite understand what it is doing but I'll post it here in case someone else does):

With original code and -O2:

L2:
movl    %esi, 28(%esp)
movl    28(%esp), %eax
movl    $LC0, (%esp)
movl    %eax, 4(%esp)
call    _printf
movl    __ZSt4cout, %eax
movl    -12(%eax), %eax
movl    __ZSt4cout+124(%eax), %ebx
testl   %ebx, %ebx
je  L10
cmpb    $0, 28(%ebx)
je  L3
movzbl  39(%ebx), %eax
L4:
movsbl  %al, %eax
addl    $715827883, %esi
movl    %eax, 4(%esp)
movl    $__ZSt4cout, (%esp)
call    __ZNSo3putEc
movl    %eax, (%esp)
call    __ZNSo5flushEv
jmp L2                  // no test

With mymanual inlining of std::endl, -O2:

L3:
movl    %ebx, 28(%esp)
movl    28(%esp), %eax
addl    $715827883, %ebx
movl    $LC0, (%esp)
movl    %eax, 4(%esp)
call    _printf
movl    $10, 4(%esp)
movl    $__ZSt4cout, (%esp)
call    __ZNSo3putEc
movl    $__ZSt4cout, (%esp)
call    __ZNSo5flushEv
cmpl    $-1431655764, %ebx
jne L3
xorl    %eax, %eax

One difference between these two is that %esi is used in the original , and %ebx in the second version; is there any difference in semantics defined between %esi and %ebx in general? (I don't know much about x86 assembly).


What I cannot get is why i value is broken by that overflow operation?

It seems that integer overflow occurs in 4th iteration (for i = 3). signed integer overflow invokes undefined behavior. In this case nothing can be predicted. The loop may iterate only 4 times or it may go to infinite or anything else!
Result may vary compiler to compiler or even for different versions of same compiler.

C11: 1.3.24 undefined behavior:

behavior for which this International Standard imposes no requirements
[ Note: Undefined behavior may be expected when this International Standard omits any explicit definition of behavior or when a program uses an erroneous construct or erroneous data. Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message). Many erroneous program constructs do not engender undefined behavior; they are required to be diagnosed. —end note ]


Another example of this error being reported in gcc is when you have a loop that executes for a constant number of iterations, but you are using the counter variable as an index into an array that has less than that number of items, such as:

int a[50], x;

for( i=0; i < 1000; i++) x = a[i];

The compiler can determine that this loop will try to access memory outside of the array 'a'. The compiler complains about this with this rather cryptic message:

iteration xxu invokes undefined behavior [-Werror=aggressive-loop-optimizations]

참고URL : https://stackoverflow.com/questions/24296571/why-does-this-loop-produce-warning-iteration-3u-invokes-undefined-behavior-an

반응형