未定義行為
未定義行為
在計算機程序設計中,未定義行為是指執行某種計算機代碼所產生的結果,這種代碼在當前程序狀態下的行為在其所使用的語言標準中沒有規定。常見於翻譯器對源代碼存在某些假設,而執行時這些假設不成立的情況。
一些編程語言中,某些情況下存在未定義行為,以C和C++最為著名。在這些語言的標準中,規定某些操作的語義是未定義的,典型的例子就是程序錯誤的情況,比如越界訪問數組元素。標準允許語言的具體實現做這樣的假設:只要是匹配標準的程序代碼,就不會出現任何類似的行為。具體到 C/C++ 中,編譯器可以選擇性地給出相應的診斷信息,但沒有對此的強制要求:針對未定義行為,語言實現作出任何反應都是正確的,類似於數字邏輯中的無關項。雖然編譯器實現可能會針對未定義行為給出診斷信息,但保證編寫的代碼中不引發未定義行為是程序員自己的責任。這種假設的成立,通常可以讓編譯器對代碼作出更多優化,同時也便於做更多的編譯期檢查和靜態程序分析。
有時候也可能存在對於未定義行為本身的限制性要求。例如,在CPU的指令集說明中可能將某些形式的指令定為未定義,但如果該CPU支持內存保護,說明中很可能會還會包含一條兜底的規則,要求任何用戶態的指令都不會讓操作系統的安全性受損;這樣一來,在執行未定義行為的指令時,就允許CPU破壞用戶寄存器,但不允許發生諸如切換到監控模式的操作。
和未指定行為(unspecified behavior)不同,未定義行為強調基於不可移植或錯誤的程序構造,或使用錯誤的數據。一個匹配標準的實現可以在假定未定義行為永遠不發生(除了顯式使用不嚴格遵守標準的擴展)的基礎上進行優化,可能導致原本存在未定義行為(例如有符號數溢出)的程序經過優化后顯示出更加明顯的錯誤(例如死循環)。因此,這種未定義行為一般應被視為bug。
如果某一操作在文檔中被定為未定義行為,編譯器就可以假設該操作在匹配標準的程序中永遠不會發生。這樣,編譯器就可以得到更多的信息,獲得更多優化程序的機會。
例如這樣的C語言代碼:
因為x是unsigned char不可能為負數,而C語言中有符號整數的溢出又是未定義行為,編譯器就可以假設執行if語句時value不可能小於 2147483600。因為這裡的if沒有副作用,條件也永遠不成立,所以編譯器就可以直接忽略if語句和對函數bar的調用。於是,上述代碼在語義上就等價於:
如果有符號整數的溢出有明確的“環繞”行為,那麼這樣的程序轉化就是非法的。
代碼越複雜,類似的優化就越難被人類發現。如果代碼同時還有其它方面的優化,例如內聯,就更難發現了。
讓有符號整數溢出未定義還有另一個好處:存儲、操作變數的值時,可以在比變數本身更大的寄存器中進行。假設源代碼中變數的類型比原生寄存器的寬度要窄(比如常見的在64位機器上的int類型),那麼編譯器就可以在生成機器碼時把這個變數當作64位有符號數,對代碼的語義沒有任何影響。反之,如果32位有符號整數的溢出有明確定義,那麼在針對64位機器編譯時,編譯器就必須插入額外的邏輯確保行為匹配預期,因為大多數機器碼指令在溢出時行為與寄存器的寬度有關。
更重要的一點是,有符號整數溢出的行為未定義,允許在編譯期檢查、靜態程序分析、運行期檢查時捕捉這類錯誤的情況;如果溢出行為有明確定義,就無法進行編譯期檢查。
嘗試修改字元串字面量會產生未定義行為:
防止這一點的方法之一是將它定義為數組而不是指針:
在C++可以使用[[標準模板庫]]中的string類型,如下所示:
某些指針操作可能導致未定義行為:
到達返回數值的函數(除main函數以外)的結尾,而沒有一個return語句,會導致未定義行為:
《C程序設計語言》在第2.12節引用下面的代碼作為未定義行為的例子:
以及
標準庫可能指定未定義行為,例如: