---
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。