Dus*_*dak 5 memory virtual-machine
将范围与内存段关联起来似乎并不困难。然后有一条汇编指令,该指令将2个整数视为“位置”和“偏移”(如果设置,则将另一个整数视为“数据”),并返回数据和错误代码。这意味着在处理阵列时,不再需要在速度和安全性/安全性之间做出选择。
另一个示例可能是一个函数,该函数验证源自特定内存范围的指令不能物理访问该范围之外的内存。如果连接到主板的所有硬件都具有此功能(并使其相互兼容),则使完美的虚拟机以与物理机几乎相同的速度运行将是微不足道的。
达斯汀·索达克(Dustin Soodak)
是的。
几十年前,Lisp 机器在程序运行时同时执行验证检查(例如类型检查和边界检查),假设程序和状态是有效的,如果检查失败则“回到过去”——不幸的是这种获得“自由”的能力当传统(即 x86)机器成为主导时,运行时验证就丢失了。
https://en.wikipedia.org/wiki/Lisp_machine
Lisp 机器与更传统的单指令添加并行运行测试。如果同时测试失败,则结果被丢弃并重新计算;这在许多情况下意味着速度提高了几个因素。这种同时检查方法也用于测试引用时数组的边界,以及其他内存管理必需品(不仅仅是垃圾收集或数组)。
幸运的是,我们终于从过去慢慢地、逐步地重新引入了这些创新——英特尔的“MPX”(内存保护扩展) x86 被引入到 Skylake 一代处理器中,用于硬件边界检查——尽管它并不完美.
(x86 在其他方面也是一种回归:IBM 的大型机在 1980 年代具有真正的硬件加速系统虚拟化——直到 2005 年我们才在 x86 上使用英特尔的“VT-x”和 AMD 的“AMD-V”扩展)。
BOUND从技术上讲,x86确实具有硬件边界检查:该BOUND指令于 1982 年在Intel 80188 中引入(以及英特尔286以上,但不在英特尔的8086,8088或80186处理器)。
虽然该BOUND指令确实提供了硬件边界检查,但我理解它间接导致了性能问题,因为它破坏了硬件分支预测器(根据 Reddit 线程,但我不确定为什么),但也因为它需要在内存中的元组- 这对性能来说很糟糕 - 我知道在运行时它并不比手动让指令执行“如果index不在范围内,[x,y]则BR向程序或操作系统发出异常信号”(因此您可能想象该BOUND指令是为手工编写汇编代码的人的便利,这在 1980 年代非常普遍)。
该BOUND指令仍然存在于今天的处理器中,但它并未包含在 AMD64 (x64) 中 - 可能是出于我上面解释的性能原因,也因为可能很少有人使用它(编译器可以轻松地将其替换为手动边界检查,无论如何它可能具有更好的性能,因为它可以使用寄存器)。
将数组边界存储在内存中的另一个缺点是其他地方的代码(不受BOUNDS检查的)可能会覆盖先前编写的另一个指针的边界并以这种方式绕过检查 - 这主要是故意尝试禁用的代码的问题安全功能(即恶意软件),但如果边界存储在堆栈中 - 并且考虑到破坏堆栈是多么容易,它的实用性就更少了。
英特尔 MPX 于 2015 年在 Skylake 架构中引入,应该出现在主流英特尔酷睿家族(包括至强、赛扬和奔腾的非 SoC 版本)的所有 Skylake 和后续处理器型号中。从 2016 年开始,英特尔还在 Goldmont 架构(Atom 以及赛扬和奔腾的 SoC 版本)中实施了 MPX。
MPX 的优越之处BOUND在于它提供了专用寄存器来存储边界范围,因此与需要内存访问的边界检查相比,边界检查应该几乎为零成本BOUND。在 Intel 486 上,BOUND指令需要 7 个周期(相比之下,即使操作数是内存地址也只CMP需要2 个周期)。在 Skylake 中,MPX 等价物 ( BNDMK,BNDCL和BNDCU) 都是 1 周期指令和BNDMK可以摊销,因为它只需要为每个新指针调用一次)。
我找不到任何关于 AMD 是否已经实现了他们自己的 MPX 版本的信息(截至 2017 年 6 月)。
不幸的是,MPX 的当前状态并不是那么乐观——Oleksenko、Kuvaiskii 等人最近发表的一篇论文。2017 年 2 月的“英特尔 MPX 解释”(PDF 链接:警告:尚未经过同行评审)有点关键:
我们的主要结论是,英特尔 MPX 是一种很有前途的技术,但尚不能广泛采用。Intel MPX 的性能开销仍然很高(平均约 50%),并且支持的基础架构存在可能导致编译或运行时错误的错误。此外,我们展示了英特尔 MPX 的设计局限性:它无法检测时间错误,在多线程代码中可能存在误报和漏报,其对内存布局的限制需要对某些程序进行大量代码更改。
还要注意,与过去的 Lisp 机器相比,英特尔 MPX 仍然是内联执行的——而在 Lisp 机器中(如果我的理解是正确的)边界检查在硬件中同时发生,如果检查失败则向后跳转;因此,只要正在运行的程序的指针不指向越界位置,那么运行时性能成本就绝对为零,所以如果您有以下 C 代码:
char arr[10];
arr[9] = 'a';
arr[8] = 'b';
Run Code Online (Sandbox Code Playgroud)
然后在 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.
Run Code Online (Sandbox Code Playgroud)
但是在 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
Run Code Online (Sandbox Code Playgroud)
我了解“侧面”指令的每个周期指令与“实际”指令不同 - 因此对指令的侧面检查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 从 5.0 到 9.1 ( https://gcc.gnu.org/wiki/Intel%20MPX%20support%20in%20the%20GCC%20compiler ) 因为它的维护负担而被删除。
Visual Studio 2015 Update 1 (2015.1) 添加了对 MPX/d2MPX开关的“实验性”支持( https://blogs.msdn.microsoft.com/vcblog/2016/01/20/visual-studio-2015-update-1-new -experimental-feature-mpx/)。Visual Studio 2017 中仍然提供支持,但微软尚未宣布它是否被视为主流(即非实验性)功能。
Clang 过去部分支持手动使用 MPX,但在 10.0 版本中完全删除了该支持
截至 2021 年 7 月,LLVM 似乎仍然能够输出 MPX 指令,但我看不到任何 MPX“通过”的证据。
英特尔 C/C++ 编译器从 15.0 版开始支持 MPX。