---
# System prepended metadata

title: 只是一則有趣(?)的 Undefined Behavior
tags: [' System Software']

---


:::warning
[nasal-demons](http://www.catb.org/jargon/html/N/nasal-demons.html): **"When the compiler encounters a given undefined construct, it is legal for it to make demons fly out of your nose"**
:::

## 問題

```cpp
#include <stdio.h>
#include <stdint.h>

const unsigned int translate[16] = {0, 1, 2, 3, 8,  9,  10, 11,
                                    4, 5, 6, 7, 12, 13, 14, 15};
static unsigned int update(int res, unsigned int i) {
    if (i >= 16)
        return -1;

    return translate[i];
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        return -1;
    }

    unsigned int val = 0;
    sscanf(argv[1], "%d", &val);

    unsigned int ans = update(val, __builtin_ctz(val) >> 1);
    printf("get ans %d\n", ans);

    return 0;
}
```

考慮上述程式，開啟 -O3 選項編譯。在給定的 `argv[1]` 是合法的非負整數的前提下，最終可能印出的 `ans` 只會是 -1 ~ 15 之間的任何數值，是否正確呢?

若從 `update` 的實作來看，不管到底 `update` 接受的輸入是甚麼，輸入的 i 若 >= 16 的數字都會直接返回 -1，否則才返回 `translate[i]`，而後者的範圍在 0 ~ 15 之間。則乍看之下前述問題的答案是正確的。

但實際上，在 Linux 上使用 gcc 11.3 編譯後，執行 `./a.out 0` 我們會得到

```
$ gcc -O3 test.c
$ ./a.out 0
get ans 990059265
```

從結果來看，我們只能猜測是發生對陣列的越界存取，也就是對 `translate[i]` 輸入的 `i` 超出 15。可是理論上 `update` 已經提前用 if 來保護對 `translate` 的存取了，這又怎麼可能會發生呢?

## 分析

也許你已經發現了一些端倪。若根據 gcc 的手冊，顯然 [`__builtin_ctz(0)`](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html#index-_005f_005fbuiltin_005fctz) 是未定義的行為，因此我們會得到的返回值是不可預期的。然而這和 `update` 又有何關係呢? 這邊我們就得一步步解釋編譯器的優化邏輯:


1. 開啟 -O3 下，編譯不必將 `update` 視為獨立編譯單元(compilation unit)，而是可以直接 inline 到 `main` 之中
2. 因為 `__builtin_ctz(0)` 是 undefined behavior，編譯器直接假設這情況不被預期發生
3. 呈上，則因為輸入到 `__builtin_ctz` 中的數字是 32 位元的非負整數，因此編譯器可以認定 `__builtin_ctz()` 能得到的結果是 0 到 31 之間的任意數字
4. 那麼 `__builtin_ctz() >> 1` 能得到的結果就是 0 到 15 之間的任意數字
5. 根據前面的推論，既然給定 `update` 的 `i` 是來自 `__builtin_ctz() >> 1`，而該結果是 0 至 15 之間的值，編譯器判斷 `if (i > = 16)` 是不必要的，可以直接移除掉
6. 因此輸入 `./a.out 0` 使得發生 `__builtin_ctz(0)` 時，後者的結果在編譯器的優化下返回 32，加上前面 if 被移除的結果，最終得到輸出 "get ans 990059265" 

## 小結

從上述問題中我們可以看到，即便我們認為自己有透過 `if` 來避免對陣列的越界存取，然而在激進的優化之下，只要程式中存在未定義行為，那麼我們仍然無法完全保證編譯器不會直接將其消除掉，進而造成前述狀況的發生。其他類似案例也可以在 [GCC undefined behaviors are getting wild](http://blog.pkh.me/p/37-gcc-undefined-behaviors-are-getting-wild.html) 一文看到。

這裡也不難看出 UB 造成的程式錯誤可能相當棘手，因為程式的執行和 C 語言中描述的邏輯已經產生歧異，嚴謹的方式上，我們可能得深入到組語才能夠探究其中端倪。不過要是對組語不是很熟悉，發生類似的情形時該怎麼除錯呢? 也許先善用 [UBSAN](https://www.kernel.org/doc/html/latest/dev-tools/ubsan.html) 排除程式中的未定義行為會是不錯的方式。