L 開完會,想要去吃晚餐的時候,忽然被 A 叫住:你能來看一個 X 公司發過來的 Issue 嗎?這有點急。
問題是這樣的:下面這段代碼和預期的行為不一樣,如果傳 0x01 0x00 0x00 0x80 進去,會回傳 0x80000001 而不是 0x00000001:
uint32_t convert(const uint8_t *restrict p) {
uint32_t x = ( p[0] +
256 * p[1] +
256 * 256 * p[2] +
256 * 256 * 256 * p[3]);
if (x > INT32_MAX) {
return (x - INT32_MAX) - 1;
} else {
return (((int)x + (int)-INT32_MAX) - 1);
}
}
X 公司的工程師上傳一個 CL 說下面的 if (...) { ... } else { ... }
似乎有問題,改成 return x ^ (-INT32_MAX - 1)
就能解決問題。
但是這一定很有問題呀。因為這份代碼已經有點年代,以前寫好的時候應該測過。如果這樣改可以解決問題,一定是因為編譯器對 if (...) { ...} else { ... }
施行不同的最佳化呀。應該要先檢查那一大段 if 有沒有 Overflow 或 Underflow 造成的 Undefined Behavior 吧?
但是 L 和 A 檢查 if 裡面的代碼,看了 10 分鐘,還是覺得 Code 沒有問題。
L 和 A 也想過:是不是 Signed / Unsigned 之間的比較造成一些問題?但是同事 B 很快就做了實驗說明 C/C++ 編器在比較相同大小的 Signed 和 Unsigned Integer 時,會將 Signed Integer 轉成 Unsigned Integer,符合程式原本的預期。
眼看沒線索 L 決定先脫離戰線吃晚餐。
L 回到戰線後,發現同事 C 也加入討論。A、B、C 三人在討論那個精妙的 XOR。其實一開始 L 沒有看懂程式碼。這個程式碼想做的事是把 0x80000000 當成 0、0x80000001 當成 1、0x7fffffff 當成 -1、0x7ffffffe 當成 -2。所以用 XOR 把 MSB 對調(即 x ^ 0x80000000
)和原本的代碼是等價的。
但是這還是很不合理呀!難道編譯器真的有 Bug?
L 表示:我們有代碼對吧?代碼在哪裡?讓我來反組譯。
... 過了 5 分鐘 ...
L 表示:編譯器生出這樣的 Code 一定很有問題呀:
00000000 <convert>:
0: 6800 ldr r0, [r0, #0]
2: f040 4000 orr.w r0, r0, #2147483648 ; 0x80000000
6: 4770 bx lr
編譯器把那一大段 p[0]
, p[1]
, p[2]
, p[3]
變成 ldr
(load word) 是很厲害沒有錯。可是那個 orr.w
是 Bitwise OR 不是 Exclusive OR 啊... 囧。
然後工程師 A 回覆編譯器可能有 Bug,希望可以結束這回合。
作為一個號稱做過編譯器的人,L 想再花一點時間生測資並回報錯誤。可是弄了幾分鐘,就是弄不出來。下面的代碼怎麼編怎麼對:
uint32_t convert(uint32_t x) {
if (x > INT32_MAX) {
return (x - INT32_MAX) - 1;
} else {
return (((int)x + (int)-INT32_MAX) - 1);
}
}
這時候才把注意力放到前面的 p[3]
:
uint32_t x = ( p[0] +
256 * p[1] +
256 * 256 * p[2] +
256 * 256 * 256 * p[3]);
這裡 256 是 signed int,p[3]
是 unsigned char。在 C/C++ 中,不同大小的整數相乘時,會先 promote 比較小的型別。此例中,p[3]
會變成數值在 0-255 的 signed int。然後因為 256 * 256 * 256 * [0-255]
會溢位,進而產生 Undefined Behavior。而 clang 就是利用這個 Undefined Behavior 來施行最佳化。這下一切都有合理的解釋。一份不知道多久以前交接下來的代碼帶有一些問題也是很合理的。
感謝 X 公司優秀工程師的努力、和同事 A、B、C、L 的討論。銀河的歷史又翻過新的一頁。
p.s. 後來發現其實只要有 clang -fsanitize=undefined
一下就抓到了...