FaultMaps 和隐式检查

动机

由托管语言运行时生成的代码往往包含出于安全考虑而必需的检查,但在实践中这些检查永远不会失败。在这种情况下,即使失败的情况成本显著增加,使非失败的情况成本更低也是有利可图的。 这种不对称性可以被利用,将此类安全检查折叠到可以可靠地产生故障的操作中(如果检查本应失败),并通过使用信号处理程序从这种故障中恢复。

例如,Java 要求在读取或写入对象之前对其进行空值检查。如果对象为 null,则必须抛出 NullPointerException,从而中断正常执行。 然而,在行为良好的 Java 程序中,解引用 null 指针的情况极为罕见,并且通常可以将空值检查折叠到附近对同一内存位置进行操作的内存操作中。

Fault Map 区段

由 LLVM 生成的关于隐式检查的信息被放在一个特殊的 “fault map” 区段中。在 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 pass

ImplicitNullChecks pass 转换显式的控制流,用于检查指针是否为 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 pass 在 codegen 期间运行。

ImplicitNullChecks pass 根据需要将条目添加到上面描述的 __llvm_faultmaps 区段。

make.implicit 元数据

将空值检查隐式化是一种激进的优化,如果过多的内存操作因此而最终导致故障,则可能会导致净性能下降。 语言运行时通常需要确保,一旦应用程序达到稳定状态,只有极少数的隐式空值检查实际上会发生故障。 实现此目的的标准方法是通过代码修补或重新编译将失败的隐式空值检查“治愈”为显式空值检查。 由此可见,显式空值检查需要满足两个要求才能有利可图地将其转换为隐式空值检查:

  1. 指针实际上为空的情况(即“失败”的情况)非常罕见。

  2. 失败路径将隐式空值检查“治愈”为显式空值检查,以便应用程序不会重复发生页面错误。

预计前端会使用 !make.implicit 元数据节点(元数据节点的实际内容将被忽略)标记满足 (1) 和 (2) 的分支。 只有标有 !make.implicit 元数据的分支才会被视为转换为隐式空值检查的候选对象。

(请注意,虽然我们可以使用分析数据来处理 (1),但处理 (2) 需要分支配置文件中不存在的一些信息。)