未定義行為
在電腦程式設計中,未定義行為(英語:undefined behavior)是指執行某種電腦代碼所產生的結果,這種代碼在當前程式狀態下的行為在其所使用的語言標準中沒有規定。常見於翻譯器對原始碼存在某些假設,而執行時這些假設不成立的情況。
一些程式語言中,某些情況下存在未定義行為,以C和C++最為著名[1]。在這些語言的標準中,規定某些操作的語意是未定義的,典型的例子就是程式錯誤的情況,比如越界訪問陣列元素。標準允許語言的具體實現做這樣的假設:只要是符合標準的程式碼,就不會出現任何類似的行為。具體到 C/C++ 中,編譯器可以選擇性地給出相應的診斷資訊,但沒有對此的強制要求:針對未定義行為,語言實現作出任何反應都是正確的,類似於數字邏輯中的無關項。雖然編譯器實現可能會針對未定義行為給出診斷資訊,但保證編寫的代碼中不引發未定義行為是程式設計師自己的責任。這種假設的成立,通常可以讓編譯器對代碼作出更多最佳化,同時也便於做更多的編譯期檢查和靜態程式分析。
有時候也可能存在對於未定義行為本身的限制性要求。例如,在CPU的指令集說明中可能將某些形式的指令定為未定義,但如果該CPU支援主記憶體保護,說明中很可能會還會包含一條兜底的規則,要求任何用戶態的指令都不會讓作業系統的安全性受損;這樣一來,在執行未定義行為的指令時,就允許CPU破壞用戶暫存器,但不允許發生諸如切換到監控模式的操作。
和未指定行為(unspecified behavior)不同,未定義行為強調基於不可移植或錯誤的程式構造,或使用錯誤的數據。一個符合標準的實現可以在假定未定義行為永遠不發生(除了顯式使用不嚴格遵守標準的擴充)的基礎上進行最佳化,可能導致原本存在未定義行為(例如有符號數溢位)的程式經過最佳化後顯示出更加明顯的錯誤(例如無窮迴圈)。因此,這種未定義行為一般應被視為bug。
好處
如果某一操作在文件中被定為未定義行為,編譯器就可以假設該操作在符合標準的程式中永遠不會發生。這樣,編譯器就可以得到更多的資訊,獲得更多最佳化程式的機會。
例如這樣的C語言代碼:
int foo(unsigned char x)
{
int value = 2147483600; /* 假设 int 是 32 位 */
value += x;
if (value < 2147483600)
bar();
return value;
}
因為 x
是 unsigned char
不可能為負數,而C語言中有符號整數的溢位又是未定義行為,編譯器就可以假設執行 if
陳述式時 value
不可能小於 2147483600。因為這裏的 if
沒有副作用,條件也永遠不成立,所以編譯器就可以直接忽略 if
陳述式和對函數 bar
的呼叫。於是,上述代碼在語意上就等價於:
int foo(unsigned char x)
{
int value = 2147483600;
value += x;
return value;
}
如果有符號整數的溢位有明確的「環繞」行為,那麼這樣的程式轉化就是非法的。
代碼越複雜,類似的最佳化就越難被人類發現。如果代碼同時還有其它方面的最佳化,例如行內展開,就更難發現了。
讓有符號整數溢位未定義還有另一個好處:儲存、操作變數的值時,可以在比變數本身更大的暫存器中進行。假設原始碼中變數的類型比原生暫存器的寬度要窄(比如常見的在64位元機器上的int類型),那麼編譯器就可以在生成機械碼時把這個變數當作64位元有符號數,對代碼的語意沒有任何影響。反之,如果32位元有符號整數的溢位有明確定義,那麼在針對64位元機器編譯時,編譯器就必須插入額外的邏輯確保行為符合預期,因為大多數機械碼指令在溢位時行為與暫存器的寬度有關。[2]
更重要的一點是,有符號整數溢位的行為未定義,允許在編譯期檢查、靜態程式分析、執行期檢查時捕捉這類錯誤的情況;如果溢位行為有明確定義,就無法進行編譯期檢查。
C和C++的未定義行為的一些例子
char * p = "wikipedia"; // C++11中错误,C++98/C++03不推荐使用
p[0] = 'W'; // 未定义行为
char p[] = "wikipedia"; /* 正确 */
p[0] = 'W';
在C++可以使用標準模板庫中的string類型,如下所示:
std::string s = "wikipedia"; /* 正确 */
s[0] = 'W';
除以零會導致未定義行為。根據 IEEE 754,float、double和long double類型的值除以零的結果是無窮大或NaN:[4]
return x/0; // 未定义行为
某些指標操作可能導致未定義行為:[5]
int arr[4] = {0, 1, 2, 3};
int* p = arr + 5; // 未定义行为
到達返回數值的函數(除main函數以外)的結尾,而沒有一個return陳述式,會導致未定義行為:
int f()
{
} /* 未定义行为 */
《C程式語言》在第2.12節參照下面的代碼作為未定義行為的例子:
printf("%d %d\n", ++n, power(2, n)); /* 未定义行为 */
以及
a[i] = i++; /* 未定义行为 */
標準庫可能指定未定義行為,例如:
int x = 1;
printf("%d\n", &x); /*未定义行为:%d预期int类型的实际参数*/
printf("%p\n", &x); /*未定义行为:%p预期void*类型的实际参数*/
printf("%p\n", (void*)&x); /*%p和void*类型的实际参数匹配,不在此引发未定义行为*/
參考資料
- ^ Lattner, Chris. What Every C Programmer Should Know About Undefined Behavior. LLVM Project Blog. LLVM.org. May 13, 2011 [May 24, 2011]. (原始內容存檔於2014-10-30).
- ^ 存档副本. [2018-06-21]. (原始內容存檔於2018-07-09).
- ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §2.13.4 String literals [lex.string] para. 2
- ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §5.6 Multiplicative operators [expr.mul] para. 4
- ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §5.7 Additive operators [expr.add] para. 5
外部連結
- The Jargon File on "nasal demons" (頁面存檔備份,存於互聯網檔案館),未定義行為的一個可能後果。