未定義行為與安全

「在我機器上能跑」不等於正確

有一句話每個程式設計師都說過、每個程式設計師最終也都會後悔:「可是在我機器上能跑啊」。它的表親是「可是它編譯過了」和「又沒有警告」。每一句都讓人覺得是程式正確的證明。但沒有一句是。理解「為什麼」是學 C 過程中最讓人成熟的領悟之一,因為在 C 裡,「看起來能跑」和「是正確的」之間的縫隙,寬到足以開過一場安全事件。

關鍵在這。「它編譯過了」只代表這段程式碼是文法上合法、編譯器能翻譯的 C——它完全沒說程式做的是不是對的事,甚至沒說它有沒有停在已定義行為內。「沒有警告」好一些,但仍然薄弱:警告抓得到一些錯誤,但編譯器並不被要求診斷未定義行為,而最危險的臭蟲根本不產生任何警告。而「在我機器上能跑」是最險惡的,因為未定義行為常常「看起來」能跑。一支讀取未初始化變數、把緩衝區溢出幾個位元組、或倚賴有號溢位的程式,可能在你的電腦、用你的編譯器、在你的最佳化等級、跑你的輸入時完美無瑕——然後在隊友的機器上、在發行建構裡、在不同 CPU 上、或單純在你沒試過的輸入上爆掉。那行為從來沒被保證過;你只是運氣好,而運氣不是你能出貨的性質。

為什麼重要:這正是未定義行為如此陰險的實務原因,也是這整個領域存在的理由。防禦之道在於用證據取代運氣:開啟警告並把它們當作錯誤(gcc -Wall -Wextra -Werror);執行消毒器(AddressSanitizer、UBSan)在執行期主動偵測未定義行為;在不只一種編譯器與最佳化等級上測試;寫測試去操練邊界輸入、而不只是順利的路徑;並對照標準的規則來推理你的程式碼,而不是對照你機器今天碰巧做了什麼。「在我機器上能跑」是一場調查的開始,永遠不是它的結束。

/* 在 -O0「能跑」,在 -O2 爆掉:未定義行為藏在裡面 */ int x; /* 未初始化 */ if (x == 0) /* 讀到垃圾值:UB */ launch(); /* 用證據取代運氣: */ $ gcc -O2 -Wall -Wextra -Werror -fsanitize=address,undefined main.c $ ./a.out /* 消毒器在執行期回報 UB */

同一段程式碼可能在你機器上過、在別處失敗。把警告當錯誤再加上消毒器,能把沉默的 UB 變成大聲、可重現的回報。

乾淨地編譯與成功地執行,只證明這一次沒有東西讓臭蟲現形。未定義行為可以隱形多年,然後在新編譯器、新輸入、或更高最佳化等級上爆掉。

又稱
it compiledworks on my machine在我機器上能跑它編譯過了