【问题标题】:Do any CPUs have hardware support for bounds checking?是否有任何 CPU 具有边界检查的硬件支持?
【发布时间】:2016-11-22 22:01:11
【问题描述】:

将范围与内存段关联起来似乎并不困难。然后有一个汇编指令,将 2 个整数视为“位置”和“偏移”(如果设置,则另一个为“数据”),并返回数据和错误代码。这意味着在使用数组时不再需要在速度和安全性/安全性之间做出选择。

另一个例子可能是一个函数,它验证源自特定内存范围的指令不能物理访问该范围之外的内存。如果连接到主板的所有硬件都具有这种能力(并且相互兼容),那么制作完美的虚拟机并以几乎与物理机相同的速度运行将是微不足道的。

达斯汀·苏达克

【问题讨论】:

    标签: memory virtual-machine


    【解决方案1】:

    是的。

    几十年前,Lisp machines 在假设程序和状态有效的情况下运行程序时执行同时验证检查(例如类型检查和边界检查),如果检查失败,则“及时”跳转 - 不幸的是,这种能力当传统(即 x86)机器占据主导地位时,获取“免费”运行时验证会丢失。

    https://en.wikipedia.org/wiki/Lisp_machine

    Lisp Machines 与更传统的单指令添加并行运行测试。如果同时测试失败,则丢弃并重新计算结果;在许多情况下,这意味着速度会增加几个因素。 这种同时检查方法也用于测试引用时数组的边界,以及其他内存管理必需品(不仅仅是垃圾收集或数组)。

    幸运的是,我们终于从过去中慢慢吸取了教训,逐渐地、零碎地重新引入这些创新 - 在 Skylake 一代处理器中引入了用于 x86 的 Intel's "MPX" (Memory Protection eXtensions) 用于硬件边界检查 - 尽管它并不完美。

    (x86 在其他方面也是一种回归:IBM 的大型机在 1980 年代拥有真正的硬件加速系统虚拟化 - 我们直到 2005 年才在 x86 上使用英特尔的“VT-x”和 AMD 的“AMD-V”扩展名)。

    x86 BOUND

    从技术上讲,x86 确实具有硬件边界检查:the BOUND instruction 于 1982 年在 Intel 80188 中引入(以及 Intel 286 及更高版本,但不是 Intel 8086、8088或 80186 处理器)。

    虽然BOUND 指令确实提供了硬件边界检查,但我理解它间接导致了性能问题,因为它破坏了硬件分支预测器(according to a Reddit thread,但我不确定为什么),而且还因为它需要边界在元组中指定 在内存中 - 这对性能来说很糟糕 - 我知道在运行时它并不比手动执行指令更快“如果index不在[x,y]范围内然后发出信号BR 程序或操作系统的异常”(因此您可能会想象添加了 BOUND 指令是为了方便手工编写汇编代码的人,这在 1980 年代很常见)。

    BOUND 指令仍然存在于当今的处理器中,但它并未包含在 AMD64 (x64) 中 - 可能是出于我上面解释的性能原因,也可能是因为可能很少有人使用它(编译器可能微不足道用手动边界检查替换它,这可能有更好的性能,因为它可以使用寄存器)。

    将数组边界存储在内存中的另一个缺点是其他地方的代码(不受BOUNDS检查的影响)可能会覆盖先前为另一个指针编写的边界并以这种方式规避检查 - 这主要是一个问题有意尝试禁用安全功能(即恶意软件)的代码,但如果边界存储在堆栈中 - 并且考虑到破坏堆栈是多么容易,它的实用性就更小了。

    英特尔 MPX

    英特尔 MPX 于 2015 年在 Skylake 架构中引入,并且应该出现在主流英特尔酷睿系列中的所有 Skylake 和后续处理器型号中(包括 Xeon,以及赛扬和奔腾的非 SoC 版本)。从 2016 年起,英特尔还在 Goldmont 架构(Atom 以及赛扬和奔腾的 SoC 版本)中实现了 MPX。

    MPX 优于 BOUND,因为它提供专用寄存器来存储边界范围,因此与需要内存访问的 BOUND 相比,边界检查应该几乎零成本。在 Intel 486 上,BOUND 指令 takes 7 cycles(与 CMP 相比,即使操作数是内存地址也需要 only 2 cycles)。在 Skylake 中,MPX 等效项(BNDMK、BNDCL 和 BNDCU)都是 1 周期指令,BNDMK 可以摊销,因为它只需要为每个新指针调用一次)。

    我找不到任何关于 AMD 是否在哪里实施了自己的 MPX 版本的信息(截至 2017 年 6 月)。

    对 MPX 的批判性思考

    不幸的是,MPX 的当前状态并不那么乐观 - a recent paper by Oleksenko, Kuvaiskii, et al. in February 2017 "Intel MPX Explained"(PDF link:警告:尚未经过同行评审)有点关键:

    我们的主要结论是,英特尔 MPX 是一种很有前途的技术,但还不能广泛采用。英特尔 MPX 的性能开销仍然很高(平均约为 50%),并且支持的基础架构存在可能导致编译或运行时错误的错误。此外,我们展示了英特尔 MPX 的设计局限性:它无法检测时间错误,在多线程代码中可能存在误报和误报,以及它的局限性 在内存布局上需要对某些程序进行大量代码更改。

    还请注意,与以前的 Lisp 机器相比,英特尔 MPX 仍然是内联执行的 - 而在 Lisp 机器中(如果我的理解是正确的)边界检查在硬件中同时发生,并具有向后追溯跳转如果检查失败;因此,只要运行程序的指针不指向越界位置,那么运行时性能成本绝对为零,所以如果你有这个 C 代码:

    char arr[10];
    arr[9] = 'a';
    arr[8] = 'b';
    

    然后在 MPX 下会执行:

    Time    Instruction              Notes
    1       BNDMK arr, arr+9         Set bounds 0 to 9.
    2       BNDCL arr                Check `arr` meets lower-bound.
    3       BNDCU arr                Check `arr` meets upper-bound.
    4       MOV 'a' arr+9            Assign 'a' to arr+9.
    5       MOV 'a' arr+8            Assign 'a' to arr+8.
    

    但是在 Lisp 机器上(如果可以神奇地将 C 编译成 Lisp...),那么计算机中的程序-阅读器-硬件能够与“实际”指令同时执行附加的“辅助”指令指令,允许“侧面”指令指示计算机在发生错误时忽略“实际”指令的结果:

    Time    Actual instruction    Side instruction
    1       MOV 'A' arr+9         ENSURE arr+9 BETWEEN arr, arr+9
    2       MOV 'A' arr+8         ENSURE arr+8 BETWEEN arr, arr+9
    

    我了解“侧面”指令的每周期指令与“实际”指令不同 - 因此,Time=1 指令的侧面检查可能仅在“实际”指令完成后完成已经进展到Time=3 - 但如果检查失败,那么它将失败指令的指令指针传递给异常处理程序,该处理程序将指示程序忽略在Time=1 之后执行的指令的结果。我不知道他们如何在没有大量内存或一些强制执行暂停的情况下实现这一点,也可能是内存围栏 - 这超出了我的回答范围,但至少在理论上是可能的。

    (请注意,在这个人为的示例中,我使用了constexpr 索引值,编译器可以证明它永远不会越界,因此会完全忽略 MPX 检查 - 所以假装它们是用户提供的变量: ) )。

    我不是 x86 方面的专家(或者在微处理器设计方面有任何经验,省去我在 UW 上的 CS500 级课程并且没有为...做功课)但我不相信并发执行尽管存在乱序执行的现有实现,但 x86 的当前设计不可能进行边界检查或“时间旅行” - 但是我可能错了。我推测如果所有指针类型都被提升为 3 元组(struct BoundedPointer<T> { T* ptr, T* min, T* max } - 从技术上讲,这已经发生在 MPX 和其他基于软件的边界检查中,因为每个受保护的指针在调用 BNDMK 时都定义了其边界)然后MMU 可以免费提供保护 - 但现在每个指针将消耗 24 个字节的内存,而不是当前的 8 个字节 - 或者与 32 位 x86 下的微不足道的 4 个字节相比 - RAM 很丰富,但仍然是有限的不应该浪费的资源。

    GCC 中的 MPX

    在 MPX 版本 5.0 到 9.1 (https://gcc.gnu.org/wiki/Intel%20MPX%20support%20in%20the%20GCC%20compiler) 由于其 maintenance burden 而被删除时,GCC 支持它。

    Visual Studio / Visual C++ 中的 MPX

    Visual Studio 2015 Update 1 (2015.1) 使用 /d2MPX 开关 (https://blogs.msdn.microsoft.com/vcblog/2016/01/20/visual-studio-2015-update-1-new-experimental-feature-mpx/) 添加了对 MPX 的“实验性”支持。 Visual Studio 2017 中仍提供支持,但微软尚未宣布它是否被视为主流(即非实验性)功能。

    Clang / LLVM 中的 MPX

    Clang 过去曾部分支持手动使用 MPX,但在 10.0 版中完全删除了该支持

    截至 2021 年 7 月,LLVM 似乎仍然能够输出 MPX 指令,但我看不到任何 MPX“通过”的证据。

    英特尔 C/C++ 编译器中的 MPX

    英特尔 C/C++ 编译器从 15.0 版开始支持 MPX。

    【讨论】:

    • 据this Phoronix article 称,英特尔“表示未来的英特尔 CPU 中可能会删除 MPX 功能”。除了明显的 Meltdown 漏洞外,MPX 似乎真的死了。
    【解决方案2】:

    Little Endian Linux、Big Endian Linux 或 AIX 操作系统上的 IBM POWER 处理器上可用的 XL 编译器具有不同的数组边界检查实现。
    使用 -qcheck 或其同义词 -C 选项可打开各种检查。 -qcheck=bounds 检查数组边界。使用此选项时,编译器会检查每个数组引用是否具有有效的下标。
    使用的硬件指令是条件陷阱,将下标与上限进行比较,如果下标太大或太小,则进行陷阱。在 C 和 C++ 中,下限为 0。在 Fortran 中,它默认为 1,但可以是任何整数。当它不为零时,从被检查的下标中减去下限,然后检查将其与上限减去下限进行比较。
    当限制在编译时已知并且足够小时,条件陷阱立即指令就足够了。当限制在执行时计算或大于 65535 时,需要比较两个寄存器的条件陷阱指令。
    由于以下几个原因,性能影响很小:
    1. 条件陷阱指令速度快。
    2. 它们在标准整数管道中执行。由于大多数 POWER CPU 有 2 或 4 个整数管道,通常会有一个空槽来放置陷阱,因此它通常基本上是零成本。
    3. 当编译器优化器可以将条件陷阱移出循环时,它只执行一次,同时检查所有循环迭代。
    4.当能证明实际下标不能超过限制时,优化器丢弃该指令。
    5. 当它可以证明下标也无效时,优化器使用无条件陷阱。
    6. 如有必要,可以在测试期间使用 -qcheck 并跳过生产构建,但开销足够小,通常不需要。
    如果我没记错的话,很久以前的一篇论文报告说,在一种情况下减速 2%,在另一种情况下减速 0%。由于该 CPU 只有一个整数流水线,因此现代 CPU 的减速应该明显减少。
    使用相同机制的其他检查可用于检测取消引用 NULL 指针、将整数除以零、使用未初始化的自动变量、专门编写的断言等。
    这不包括所有类型的无效内存使用,但它确实处理了最常见的类型,并且非常高效,并且非常易于使用。

    【讨论】:

      【解决方案3】:

      GCC 支持 -fbounds-check 用于类似目的,但目前它仅适用于 Fortran 前端 (gfortran)。

      【讨论】:

      • 作者的问题是关于 CPU 和硬件绑定检查,而不是编译器能够生成软件运行时检查
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-05-14
      • 1970-01-01
      • 1970-01-01
      • 2011-09-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多