錯誤映射和隱式檢查

動機

由託管語言執行階段生成的程式碼往往包含為了安全起見而必須進行的檢查,但在實務上卻永遠不會失敗。在這種情況下,即使會使失敗的情況變得更加昂貴,但使不失敗的情況更便宜是有利的。這種不對稱性可以通過將這些安全檢查摺疊到操作中來加以利用,如果檢查失敗,這些操作可以可靠地發生錯誤,並使用信號處理程序從此類錯誤中恢復。

例如,Java 要求在從物件讀取或寫入物件之前對其進行空值檢查。如果物件為 null,則必須拋出 NullPointerException,中斷正常執行。然而,在實務上,在行為良好的 Java 程式中,解引用 null 指標的情況極為罕見,而且通常可以將空值檢查摺疊到對相同記憶體位置進行操作的鄰近記憶體操作中。

錯誤映射區段

有關 LLVM 生成的隱式檢查的信息會放入一個特殊的“錯誤映射”區段中。在 Darwin 上,此區段名為 __llvm_faultmaps

此區段的格式為

Header {
  uint8  : Fault Map Version (current version is 1)
  uint8  : Reserved (expected to be 0)
  uint16 : Reserved (expected to be 0)
}
uint32 : NumFunctions
FunctionInfo[NumFunctions] {
  uint64 : FunctionAddress
  uint32 : NumFaultingPCs
  uint32 : Reserved (expected to be 0)
  FunctionFaultInfo[NumFaultingPCs] {
    uint32  : FaultKind
    uint32  : FaultingPCOffset
    uint32  : HandlerPCOffset
  }
}

FailtKind 描述預期錯誤的原因。目前支援三種錯誤

  1. FaultMaps::FaultingLoad - 由於從記憶體載入而導致的錯誤。

  2. FaultMaps::FaultingLoadStore - 由於指令載入和儲存而導致的錯誤。

  3. FaultMaps::FaultingStore - 由於儲存到記憶體而導致的錯誤。

ImplicitNullChecks 階段

ImplicitNullChecks 階段會轉換顯式控制流程,以檢查指標是否為 null,例如

  %ptr = call i32* @get_ptr()
  %ptr_is_null = icmp i32* %ptr, null
  br i1 %ptr_is_null, label %is_null, label %not_null, !make.implicit !0

not_null:
  %t = load i32, i32* %ptr
  br label %do_something_with_t

is_null:
  call void @HFC()
  unreachable

!0 = !{}

轉換為透過被空值檢查的指標載入或儲存指令中隱含的控制流程

  %ptr = call i32* @get_ptr()
  %t = load i32, i32* %ptr  ;; handler-pc = label %is_null
  br label %do_something_with_t

is_null:
  call void @HFC()
  unreachable

此轉換發生在 MachineInstr 層級,而不是 LLVM IR 層級(因此上述範例僅供參考,並非逐字)。如果將 -enable-implicit-null-checks 傳遞給 llc,則 ImplicitNullChecks 階段會在程式碼生成期間執行。

ImplicitNullChecks 階段會根據需要在上述的 __llvm_faultmaps 段落中加入項目。

make.implicit 中繼資料

將空值檢查隱式化是一種激進的優化,如果因為它而導致太多記憶體操作最終發生錯誤,則可能會導致整體效能下降。語言執行環境通常需要確保在應用程式達到穩定狀態後,實際上只有極少數的隱式空值檢查會發生錯誤。實現此目的的標準方法是透過程式碼修補或重新編譯將失敗的隱式空值檢查修復為顯式空值檢查。因此,顯式空值檢查需要滿足兩個條件,才能使其轉換為隱式空值檢查:

  1. 指標實際上為空值的情況(即「失敗」的情況)極為罕見。

  2. 失敗路徑會將隱式空值檢查修復為顯式空值檢查,以便應用程式不會重複出現分頁錯誤。

前端應使用 !make.implicit 中繼資料節點標記滿足 (1) 和 (2) 的分支(中繼資料節點的實際內容將被忽略)。只有標記了 !make.implicit 中繼資料的分支才會被視為轉換為隱式空值檢查的候選對象。

(請注意,雖然我們可以使用分析資料來處理 (1),但處理 (2) 需要一些分支設定檔中不存在的資訊。)