在做CSAPP的datalab發現的一個問題,代碼如下

int isTmax(int x)
{
int y = x + 1;
int z = y + y;
// printf("y = %d
", y);
// printf("z = %d
", z);
return (!z) (!!y);
}

int main()
{
printf("isTmax = %d
", isTmax(2147483647));
return 0;
}

當我注釋掉printf時,返回值是0,當我放開注釋後,

y = -2147483648

z = 0

isTmax = 1

這就是我預想的情況,應該返回1的,不知道為什麼會出現這種情況。

使用的編譯選項是gcc -O -Wall -m32 -lm -o,編譯環境是虛擬機的debian 10 64位。

我又把它放到VS2019運行,即使沒有注釋也是返回的1和我預想的一樣。


有符號數溢出是UB,所以我們直接通過編譯器生成的彙編來看看不同編譯器的處理邏輯吧。

TL;DR:

gcc 10.1 -O2 -m32,注釋掉 printf:

輸出:

isTmax = 0

可以看到這個函數直接被優化成了 return 0,因為在編譯器在假定有符號數不會溢出的情況下,認為「一個非0有符號數數的兩倍是0」這種情況是不會發生的,所以返回了 0。

gcc 10.1 -O2 -m32,不注釋掉 printf:

輸出:

y = -2147483648
z = 0
isTmax = 1

可以發現因為 printf 需要輸出所以求值是必要的,然後 gcc 選擇了老老實實用前面求出的結果算一遍,所以結果是 1。

(實際上和-m32/-m64沒有關係,64位的編譯結果是一樣的)


再來看 clang。注釋掉 printf 的結果是一樣的(輸出 0),這裡就不再複述了。

clang 10.0.0 -O2 -m32,不注釋掉 printf:

輸出:

y = -2147483648
z = 0
isTmax = 0

可以發現 clang 這裡與 gcc 的選擇有所不同,在完成 printf 所需的計算之後依然是直接返回了 0。


最後是 msvc。

msvc 19.24 /O2:

輸出:

isTmax = 1

可以發現即使注釋了 printf,msvc 也選擇老老實實算了一遍,結果也是預料之中的 1。不注釋 printf 的結果也是顯而易見的。

總之在發生UB的情況下,不同編譯器產生的結果有所不同是很正常的,不要太在意啦。


到godbolt上測試了一下,gcc8.1開始,-O就能復現錯誤,而在此之前,-O2才能復現錯誤。

https://godbolt.org/z/4MLJE-?

godbolt.org

既然是優化的問題……那就比較容易解釋了:

有符號整數溢出的結果是undefiend behavior,雖然平時我們都認為有符號整數是補碼 ,但標準並沒有規定(C++20開始限定是補碼)。

基礎類型 - cppreference.com?

zh.cppreference.com

所以在人看來,2147483647(0x7fffffff)+1=-2147483648(0x80000000)。

但在開了優化的編譯器看來,有符號整型溢出是undefined,2147483647(0x7fffffff)+1=0。

不過有意思的是,clang和gcc就算加了-std=c++2a也是一樣優化到0。不是說從gcc9和clang9開始就支持P1236R1了嘛?


不知道,不過我猜,就算加了m32,可能優化部分算常數摺疊沒有嚴格根編譯結果吻合。


有符號數運算溢出是未定義行為吧?

那編譯器輸出什麼甚至編譯出的程序是格式化硬碟都是符合標準的(


有點意思,試了一下是 -O 的鍋:

可以看到整個 isTmax 被優化了。使用 -O0 的話結果是 1:

最早出現這個現象的是 gcc 8,7.5 的輸出還是 1:

C++ 的整數溢出是 UB,所以在函數裏計算時需要使用更多位的整數存儲中間結果,或者換一門對溢出行為有明確定義的語言。


被常數優化了:

108 0000000000400522 &:
109 400522: b8 00 00 00 00 mov $0x0,%eax
110 400527: c3 retq


安卓也是基於 linux 系統,感興趣的可以下個 Termux ,記得哪個 lab 會有報錯,神奇,可移植性有點高


gcc 加參數-m 32 會讓你的程序用32位進行編譯,參數溢出了,具體參數是多少你可以把y打出來看看


推薦閱讀:
相關文章