一個 Undefined Behavior 的故事

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 一下就抓到了...