簡體   English   中英

為什么這個循環會產生“警告:迭代 3u 調用未定義行為”並輸出超過 4 行?

[英]Why does this loop produce “warning: iteration 3u invokes undefined behavior” and output more than 4 lines?

編譯這個:

#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++ 實現都忽略整數溢出。 除以零的處理、使用零除數形成余數以及所有浮點異常的處理因機器而異,通常可以通過庫函數進行調整。 ——尾注]

您使用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)因為i的任何大於 2 的值的每次迭代都有未定義的行為 ->(2)我們可以假設i <= 2用於優化目的 ->(3)循環條件總是true -> (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在溢出時捕獲程序,
    • Clang 編譯器在這方面非常出色: -fcatch-undefined-behavior捕獲了很多未定義行為的實例(注意: "a lot of" != "all of them"

我有一個不是我寫的程序,需要明天發貨! 幫助!!!!!!111oneone

使用 gcc 的-fwrapv

此選項指示編譯器假設加法、減法和乘法的有符號算術溢出使用二進制補碼表示環繞。

1 - 此規則不適用於“無符號整數溢出”,如第 3.9.1.4 節所述

聲明為無符號的無符號整數應遵守算術模 2 n的法則,其中 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段,知道有符號整數溢出是未定義的行為,答案很長,它說:

如果在對表達式求值期間,結果未在數學上定義或不在其類型的可表示值范圍內,則行為為 undefined [ 注意:大多數現有的 C++ 實現都忽略整數溢出。 除以零的處理,使用零除數形成余數,以及所有浮點異常在機器之間有所不同,通常可以通過庫函數進行調整。 ——尾注

我們知道標准說未定義的行為是不可預測的,根據定義附帶的注釋說:

[注意:當本國際標准省略任何明確的行為定義或程序使用錯誤的構造或錯誤的數據時,可能會出現未定義的行為。 允許的未定義行為的范圍從完全忽略情況並產生不可預測的結果,在翻譯或程序執行期間以環境特征的文件化方式(有或沒有發布診斷消息),到終止翻譯或執行(通過發布診斷消息)。 許多錯誤的程序結構不會產生未定義的行為; 他們需要被診斷。 ——尾注]

但是gcc優化器到底能做什么來把它變成一個無限循環呢? 這聽起來很古怪。 但幸運的是gcc在警告中給了我們一個線索來解決它:

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

線索是Waggressive-loop-optimizations ,這是什么意思? 對我們來說幸運的是,這不是這種優化第一次以這種方式破壞代碼,我們很幸運,因為John RegehrGCC 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 中,創建一個指向數組末尾后一個位置的元素的指針是合法的,但該指針不能被取消引用。

后來說:

詳細來說,這是發生了什么。 AC 編譯器在看到 d[++k] 時,可以假設 k 的遞增值在數組邊界內,否則會發生未定義的行為。 對於這里的代碼, GCC 可以推斷出 k 的范圍是 0..15。 稍后,當 GCC 看到 k<16 時,它對自己說:“啊哈——這個表達式總是正確的,所以我們有一個無限循環。” 這里的情況,編譯器使用定義良好的假設來推斷有用的數據流事實,

所以編譯器在某些情況下必須做的是假設因為有符號整數溢出是未定義的行為,那么i必須始終小於4 ,因此我們有一個無限循環。

他解釋說,這與臭名昭著的Linux 內核空指針檢查刪除非常相似,其中看到以下代碼:

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

gcc推斷,因為ss->f;被尊重s->f; 並且由於取消引用空指針是未定義的行為,因此s不能為空,因此優化了下一行的if (!s)檢查。

這里的教訓是,現代優化器非常熱衷於利用未定義的行為,而且很可能只會變得更加激進。 很明顯,通過幾個例子,我們可以看到優化器做了一些對程序員來說完全不合理的事情,但從優化器的角度回想起來是有道理的。

tl;dr代碼生成一個測試,整數+正整數==負整數 通常優化器不會優化這個,但是在接下來使用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是 2 的補碼中的4 * 715827882

點擊-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向上移動。

如果我們715827883重新編譯,那么除了更改的數字和測試值之外, 715827883版本是相同的。 但是,-O2 然后進行了更改:

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

有人的地方就有cmpl $-1431655764, %esi-O1 ,該行已經為去除-O2 優化器必須決定將715827883添加到%esi永遠不能等於-1431655764

這很令人費解。 將其添加到INT_MIN+1確實會生成預期的結果,因此優化器肯定已經決定%esi永遠不會是INT_MIN+1並且我不確定為什么會這樣決定。

在工作示例中,得出將715827882添加到數字不能等於INT_MIN + 715827882 - 2結論似乎同樣有效! (這只有在實際發生回繞時才有可能),但它並沒有優化該示例中的線路。


我使用的代碼是:

#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);
    }
}

如果std::endl(std::cout)被刪除,則優化不再發生。 實際上用std::cout.put('\\n'); std::flush(std::cout);替換它std::cout.put('\\n'); std::flush(std::cout); std::cout.put('\\n'); std::flush(std::cout); 也會導致優化不會發生,即使std::endl是內聯的。

std::endl的內聯似乎影響了循環結構的早期部分(我不太明白它在做什么,但我會在這里發布以防其他人這樣做):

使用原始代碼和-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

使用std::endl-O2 mymanual 內聯:

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

這兩者之間的一個區別是%esi在原始版本中使用,而%ebx在第二個版本中使用; 一般來說, %esi%ebx之間定義的語義有什么區別嗎? (我不太了解 x86 匯編)。

在 gcc 中報告此錯誤的另一個示例是,當您有一個循環執行恆定次數的迭代時,但您使用 counter 變量作為索引,該數組的項目數少於該數量,例如:

int a[50], x;

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

編譯器可以確定此循環將嘗試訪問數組“a”之外的內存。 編譯器用這個相當神秘的消息抱怨這個:

迭代 xxu 調用未定義的行為 [-Werror=aggressive-loop-optimizations]

我無法得到的是為什么我的值被溢出操作破壞了?

似乎整數溢出發生在第 4 次迭代(對於i = 3 )。 signed整數溢出調用未定義的行為 在這種情況下,什么都無法預測。 循環可能只迭代4次,也可能無限循環或其他任何東西!
結果可能因編譯器而異,甚至對於同一編譯器的不同版本。

C11:1.3.24 未定義行為:

本國際標准沒有要求的行為
[注意:當本國際標准省略任何明確的行為定義或程序使用錯誤的構造或錯誤的數據時,可能會出現未定義的行為。 允許的未定義行為的范圍從完全忽略情況並產生不可預測的結果,在翻譯或程序執行期間以環境特征的文件化方式(有或沒有發布診斷消息),到終止翻譯或執行(通過發布診斷消息) 許多錯誤的程序結構不會產生未定義的行為; 他們需要被診斷。 ——尾注]

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM