未定義行為

程序语言标准中没有规定的代码所产生的的结果

計算機程序設計中,未定義行為(英語:undefined behavior)是指執行某種計算機代碼所產生的結果,這種代碼在當前程序狀態下的行為在其所使用的語言標準英語Programming_language_specification中沒有規定。常見於翻譯器源代碼存在某些假設,而執行時這些假設不成立的情況。

一些編程語言中,某些情況下存在未定義行為,以CC++最為著名[1]。在這些語言的標準中,規定某些操作的語義是未定義的,典型的例子就是程序錯誤的情況,比如越界訪問數組元素。標準允許語言的具體實現做這樣的假設:只要是符合標準的程序代碼,就不會出現任何類似的行為。具體到 C/C++ 中,編譯器可以選擇性地給出相應的診斷信息,但沒有對此的強制要求:針對未定義行為,語言實現作出任何反應都是正確的,類似於數字邏輯中的無關項英語Don't-care term。雖然編譯器實現可能會針對未定義行為給出診斷信息,但保證編寫的代碼中不引發未定義行為是程序員自己的責任。這種假設的成立,通常可以讓編譯器對代碼作出更多優化,同時也便於做更多的編譯期檢查和靜態程序分析

有時候也可能存在對於未定義行為本身的限制性要求。例如,在CPU指令集說明中可能將某些形式的指令定為未定義,但如果該CPU支持內存保護,說明中很可能會還會包含一條兜底的規則,要求任何用戶態的指令都不會讓操作系統的安全性受損;這樣一來,在執行未定義行為的指令時,就允許CPU破壞用戶寄存器,但不允許發生諸如切換到監控模式的操作。

未指定行為英語unspecified behavior(unspecified behavior)不同,未定義行為強調基於不可移植或錯誤的程序構造,或使用錯誤的數據。一個符合標準的實現可以在假定未定義行為永遠不發生(除了顯式使用不嚴格遵守標準的擴展)的基礎上進行優化,可能導致原本存在未定義行為(例如有符號數溢出)的程序經過優化後顯示出更加明顯的錯誤(例如死循環)。因此,這種未定義行為一般應被視為bug。

好處

如果某一操作在文檔中被定為未定義行為,編譯器就可以假設該操作在符合標準的程序中永遠不會發生。這樣,編譯器就可以得到更多的信息,獲得更多優化程序的機會。

例如這樣的C語言代碼:

int foo(unsigned char x)
{
     int value = 2147483600; /* 假设 int 是 32 位 */
     value += x;
     if (value < 2147483600)
        bar();
     return value;
}

因為 xunsigned 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++的未定義行為的一些例子

嘗試修改字符串字面量英語string literal會產生未定義行為:[3]

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*类型的实际参数匹配,不在此引发未定义行为*/

參考資料

  1. ^ 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). 
  2. ^ 存档副本. [2018-06-21]. (原始內容存檔於2018-07-09). 
  3. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §2.13.4 String literals [lex.string] para. 2
  4. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §5.6 Multiplicative operators [expr.mul] para. 4
  5. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §5.7 Additive operators [expr.add] para. 5

外部連結