天天看點

未定義行為 != 不安全的程式設計

原文位址:https://blog.regehr.org/archives/1467

作者:John Regehr

在C與C++裡,未定義行為(UB)對開發者來說是一個清晰且現實的危險,特别是當他們編寫将在一個信任邊界執行的代碼時。一類不那麼為人所知的未定義行為存在于大多數優化、領先的編譯器的中間表達(IR)裡。例如,除了讓你一臉茫然的C形式的UB外,LLVMIR還增加了undef與poison。當人們開始意識到這時,一個典型的反應是:“呃,為什麼?LLVM就像C那麼糟!”本文解釋為什麼這個反應是不正确的。

未定義行為是設計決策的結果:在系統的特定一層避免徹底陷入程式的錯誤。避免這些錯誤的責任委托給更高層面的抽象。例如,顯然一個安全的程式設計語言可以被編譯為機器碼,同樣顯而易見,不安全的機器碼沒有辦法對由語言實作做出的高階保證做出妥協。Swift與Rust都被編譯為LLVM IR;它們的某些安全保證由生成代碼裡的動态檢查來實施,其他保證則通過類型檢查,它們在LLVM層面沒有表示。任一方式,對Swift與Rust的安全子集,在LLVM層面的UB都不是問題,也無從檢測。如果開發環境中的某個工具確定不會執行UB,甚至C也可以被安全使用。L4.verified就是這樣的項目。

未定義行為的本質是避免強制綁定錯誤檢查與非安全操作的自由(The essence of undefined behavior is the freedom to avoid a forcedcoupling between error checks and unsafe operations)。一旦松綁,檢查可以被優化,例如通過提升出循環或完全消除。在設計良好的IR中,留下的非安全操作可以被映射到很少或沒有開銷的基本處理器操作。作為一個具體的例子,考慮這個Swift代碼:

func add(a : Int, b :Int)->Int {

  return (a & 0xffff) + (b & 0xffff);

}

盡管在整數溢出處,Swift的實作必須陷入,編譯器觀察到溢出是不可能的,釋出這個LLVM IR:

define i64 @add(i64 %a,i64 %b) {

  %0 = and i64 %a, 65535

  %1 = and i64 %b, 65535

  %2 = add nuw nsw i64 %0, %1

  ret i64 %2

}

不僅被檢查的加法操作被降級為沒有檢查的加法,而且add指令被标記上LLVM的nsw與nuw屬性,表示有符号與無符号溢出都是未定義的。單獨地,這些屬性不提供任何利益,但在這個函數被内聯後,它們使額外的優化成為可能。當Swift基準測試集被編譯到LLVM,大約八分之一加法指令具有一個表示整數溢出是未定義的屬性。

在這個特别的例子裡,nsw與nuw屬性是重複的,因為一個優化遍可以重新推導出add不會溢出這個事實。不過,通過避免可能需要代價高昂的靜态分析來重新發現已知的程式事實,這些屬性以及其他類似的屬性通常展現了自己的價值。同樣,某些事實在後面不能被重新發現,甚至在理論上,因為資訊已經在某些編譯步驟裡丢失。

總的來說,在程式員可見的抽象表示裡的未定義行為代表一個積極與危險的權衡:它犧牲了程式的正确性,以支援性能以及編譯器的簡單化。另一方面,在系統更底層的UB,比如機器碼或者一個編譯器IR,是一個内部的設計選擇,無需對程式員所面向的系統部分有任何影響。這種UB隻是要求我們接受:安全性檢查可用于剔除相應的非安全操作,以提供高效的執行。

繼續閱讀