---
# System prepended metadata

title: 'C 語言: 未定義行為'
tags: [NCKU Linux Kernel Internals, ' C語言']

---

---
tags: NCKU Linux Kernel Internals, C語言
---

# C 語言: 未定義行為

[你所不知道的C語言: 未定義行為篇](https://hackmd.io/@sysprog/c-undefined-behavior?type=view)

## 甚麼是 Undefined behavior？
* [Undefined behavior](https://en.wikipedia.org/wiki/Undefined_behavior): 語言規範書(例如 C 語言的 [ISO/IEC 9899](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1548.pdf?fbclid=IwAR2k5K3ZPS4CzQ9AbVS96QD-5N2UQyE23Ui4ic270JX5Df3pEXJkg1cBDHA)) 並未明確規範某種程式碼的執行會產生的結果。
* [Unspecified behavior](https://en.wikipedia.org/wiki/Unspecified_behavior): 語言規範中雖沒有規定結果和應實現的行為，但可以根據平台或者編譯器的文檔得知其行為（例如[ABI](https://en.wikipedia.org/wiki/Application_binary_interface))。

語言上，未定義行為提供編譯器最佳化與靜態分析的空間，同時這也代表程式的撰寫者需為自己的行為負責。

## 案例整理
參考 [Undefined Behavior and Compiler Optimizations](https://www.slideshare.net/linaroorg/bkk16503-undefined-behavior-and-compiler-optimizations-why-your-program-stopped-working-with-a-newer-compiler) 中的幾個範例。

:::warning
:warning:  需注意畢竟是 undefined behavior，所以也不是所有範例的狀況都可以重現的 XD 

需視編譯器的版本等因素而定。
:::
### Case `1`
首先可以看到以下程式，在 gcc-4.8 編譯後的程式碼會是一個無窮迴圈。

```c=
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;
} 
```

* 在 C 語言中並沒有真正的陣列，因此 `d[++k]` 等價於 `(*((d) + (++k)))`
* 因此 `d[k]` 中 k 超出原本陣列宣告的範圍是合法的，但是會產生 undefined bahavior
* 基於程式撰寫者要為自己的程式負責的假設，編譯器會認為 k >= 16 是不合理的陣列操作，結果把程式最佳化成了一個無窮迴圈!

### Case `2`
以另一個程式為例

```c=
#include <stdio.h>
int foo (int a) {
    if (a + 100 > a)
        printf("%d GT %d\n", a + 100, a);
    else
        printf("%d LT %d\n", a + 100, a);
    return 0;
}
int main () { foo(100); foo(0x7fffffff); return 0; }
```


考慮到 32 bits 的 int，針對上述程式，如果我們用
> gcc  -O0 *.c 

來編譯，得到的結果會是
```
200 GT 100
-2147483549 LT 2147483647
```

乍看之下好像沒甚麼問題，但是如果如果進行最佳化
> gcc -O2 *.c

則會得到

```
200 GT 100
-2147483549 GT 2147483647
```
::: danger
我用 gcc 9.3.0 測試的時候不管有沒有優化都是得到 `-2147483549 GT 2147483647`
:::
原因是，在不考慮 overflow 的情形下，`a + 100` 理應始終大於 `a`，因此編譯器會直接把第5、6 行最佳化掉。
:::info
可以加入 -fno-strict-overflow 和 -fwrapv 避免最佳化把該兩行移除。
:::

### Case `3`
需注意程式中踩到未定義行為可不是只是執行結果錯誤而已，甚至可能會發生安全性的疑慮！如 [GCC and pointer overflows](https://lwn.net/Articles/278137/) 中提及的案例。

```c=
 char buffer[BUFLEN];
    char *buffer_end = buffer + BUFLEN;

    /* ... */
    unsigned int len;

    if (buffer + len >= buffer_end)
	die_a_gory_death("len is out of range\n");
```

上述的程式確保了 `buffer + len` 不會存取超出 `buffer_end` 的範圍。但 "細心" 的程式撰寫者可能會想到： 如果惡意的攻擊把 `len` 設很大，有可能產生 overflow，從而可能存取 address 小於 `buffer` 的範圍，於是把判斷條件改成。

```c=
if (buffer + len >= buffer_end || buffer + len < buffer)
	loud_screaming_panic("len is out of range\n");
```

看起來世界好像變得和平了？很不幸的，由於編譯會認為 `buffer + len < buffer` 是恆成立的 statement，而把判斷式優化為：

```c=
if (buffer + len >= buffer_end)
    die_a_gory_death("len is out of range\n"); 
```

文章中提到，最好的方法應該是，避免檢查 pointer 的範圍，而是直接檢查 `len` 是否在合法的範圍下，類似下面的程式。
```c=
if (len >= BUFLEN)
        launch_photon_torpedoes("buffer overflow attempt thwarted\n");
```

### Case `4`
```c=
#include <stdio.h>
int foo(int x, int y) {
    x >>= (sizeof(int) << y);
    return x;
}
int main () {
    printf("%d\n", foo(1000, 3));
    return 0;
 }
```
考慮上述程式，如果我們用
> gcc  -O0 *.c 

來編譯，得到的結果會是 `1000`。

但是如果如果進行最佳化
> gcc -O2 *.c

則會得到 `0`。

由於 `sizeof(int) = 4`，而 `4 << 3 = 32`。如果預期 `>>` 是從右邊移出去出的位元會從左邊補進來的話，那得到的就是原始的 `x`。然而對於最佳化來說，右移超過 int 的長度(32 位元) 就等於是在做歸零，因此 `foo` 會直接被最佳化成 return 0。

:::info
可以使用 -fsanitize=undefined 來得到錯誤訊息
:::

:::danger
使用 gcc 9.3.0 測試時，加入 -fsanitize=undefined 雖然會得到報錯訊息，但 -O2 的結果仍會得到 0
:::

### Case `5`
這是個有趣的案例。可以在以下程式看到，或許是因為程式撰寫者不小心寫錯程式，當 k = 0 時，事實上是會發生有問題的操作的。

```c=
#include <stdio.h>
int testdiv(int i, int k) {
    if (!k)
        return 1/k; 
        
    return 1;
}
int main() {
    int i = testdiv(1, 0); 
    printf("%d\n",i);
    return i;
}
```
如果用

> gcc  -O0 *.c 

來編譯，那麼就會得到 Floating point exception，這是很合理的狀況。但假如我們加入最佳化
> gcc  -O2 *.c 

結果，竟然會平安無事的印出 `1`！這是因為編譯器預設 k 為除數，而判定 k 不該等於 0，因此最佳時便將 3、4 行直接移除，testdiv 變成只會 return 1。

