【问题标题】:Array bounds checks on 64-bit hardware using hardware memory-protection使用硬件内存保护对 64 位硬件进行数组边界检查
【发布时间】:2015-06-16 09:38:19
【问题描述】:

我在hacks.mozilla.org 上阅读有关 64 位 Firefox 版本的博客。

作者说:

对于asm.js 代码,增加的地址空间还允许我们使用硬件内存保护来安全地从asm.js 堆访问中删除边界检查。收益非常显着:arewefastyet.com 上报告的 asmjs-apps-*-throughput 测试为 8%-17%。

我试图了解 64 位硬件如何对 C/C++ 进行自动边界检查(假设编译器支持硬件)。我在 SO 中找不到任何答案。我找到了one technical paper on this subject,但我无法理解这是如何完成的。

有人能解释一下 64 位硬件辅助边界检查吗?

【问题讨论】:

  • 论文中的哪些内容您不清楚?你了解虚拟内存映射的工作原理吗?
  • @Sneftel,我知道他们正在使用 64 位的巨大虚拟页表来执行此操作,我将再次阅读论文以解决这个问题。

标签: c++ c x86-64 sandbox virtual-memory


【解决方案1】:

大多数现代 CPU 实现虚拟寻址/虚拟内存 - 当程序引用特定地址时,该地址是虚拟的;到物理页面的映射(如果有)由 CPU 的 MMU(内存管理单元)实现。 CPU 通过在为当前进程设置的操作系统中查找page table 将每个虚拟地址转换为物理地址。这些查找由TLB 缓存,因此大多数时候没有额外的延迟。 (在一些非 x86 CPU 设计中,TLB 未命中由操作系统在软件中处理。)

所以我的程序访问地址 0x8050,它位于虚拟页面 8 中(假设标准的 4096 字节 (0x1000) 页面大小)。 CPU 看到虚拟页面 8 映射到物理页面 200,因此在物理地址200 * 4096 + 0x50 == 0xC8050 处执行读取。

当 CPU 没有该虚拟地址的 TLB 映射时会发生什么?这种事情经常发生,因为 TLB 的大小有限。答案是 CPU 产生 page fault,由 OS 处理。

页面错误会导致几种结果:

  • 第一,操作系统可以说“哦,它只是不在 TLB 中,因为我装不下它”。操作系统从 TLB 中驱逐一个条目,并使用进程的页表映射填充新条目,然后让进程继续运行。在中等负载的机器上,这种情况每秒会发生数千次。 (在具有硬件 TLB 未命中处理的 CPU 上,例如 x86,这种情况是在硬件中处理的,甚至不是“次要”页面错误。)
  • 第二,操作系统可以说“哦,那个虚拟页面现在没有映射,因为它正在使用的物理页面被交换到磁盘,因为我的内存不足”。操作系统挂起进程,找到一些要使用的内存(可能通过换出一些其他虚拟映射),将磁盘读取排队等待请求的物理内存,当磁盘读取完成时,使用新填充的页表映射恢复进程。 (这是"major" page fault。)
  • 三,进程正在尝试访问不存在映射的内存 - 它正在读取不应该的内存。这通常称为分段错误。

相关情况是第 3 种情况。当发生段错误时,操作系统的默认行为是中止进程并执行诸如写出核心文件之类的操作。但是,允许进程捕获自己的段错误并尝试处理它们,甚至可能不停止。这就是事情变得有趣的地方。

我们可以利用这一点来执行“硬件加速”索引检查,但我们在尝试这样做时遇到了更多的绊脚石。

首先,总体思路:对于每个数组,我们将其放在自己的虚拟内存区域中,所有包含数组数据的页面都照常映射。在真实数组数据的两侧,我们创建了不可读和不可写的虚拟页面映射。如果您尝试读取数组之外的内容,则会产生页面错误。编译器在编写程序时会插入自己的页错误处理程序,并处理页错误,将其变成索引越界异常。

第一个障碍是我们只能将整个页面标记为可读或不可读。数组大小可能不是页面大小的偶数倍,所以我们遇到了一个问题——我们不能在数组末尾之前和之后放置栅栏。我们能做的最好的事情是在数组开始之前或数组结束之后在数组和最近的“栅栏”页面之间留一个小间隙。

他们如何解决这个问题?好吧,在 Java 的情况下,编译执行负索引的代码并不容易。如果是这样,无论如何都没有关系,因为负索引被视为无符号,这将索引置于数组开头的位置,这意味着它很可能会命中未映射的内存并且无论如何都会导致错误.

所以他们所做的就是对齐数组,使数组的末尾正好与页面的末尾对接,就像这样('-' 表示未映射,'+' 表示已映射):

-----------++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
|  Page 1  |  Page 2  |  Page 3  |  Page 4  |  Page 5  |  Page 6  |  Page 7  | ...
                 |----------------array---------------------------|

现在,如果索引超出了数组的末尾,它将到达未映射的第 7 页,这将导致页面错误,这将变成索引越界异常。如果索引在数组的开头之前(也就是说,它是负数),那么因为它被视为一个无符号值,它会变得非常大并且是正数,使我们再次远远超过第 7 页,导致读取未映射的内存,导致页面错误,这将再次变成索引越界异常。

第 2 个障碍 是我们确实应该在映射下一个对象之前在数组末尾留下 大量 未映射的虚拟内存,否则,如果index 超出范围,但是远远超出范围,它可能会命中有效页面并且不会导致 index-out-of-bounds 异常,而是会读取或写入任意内存。

为了解决这个问题,我们只使用了大量的虚拟内存——我们将每个数组放入自己的 4 GiB 内存区域,其中只有前 N 几页实际映射。我们可以这样做,因为我们只是在这里使用地址空间,而不是实际的物理内存。一个 64 位进程有大约 40 亿块 4 GiB 的内存区域,所以在我们用完之前我们有足够的地址空间可以使用。在 32 位 CPU 或进程上,我们可以使用的地址空间非常少,因此这种技术不太可行。事实上,今天的许多 32 位程序都用完了虚拟地址空间,只是试图访问真实内存,更不用说尝试在该空间中映射空的“栅栏”页面以尝试用作“硬件加速”索引范围检查。

【讨论】:

  • 很好的解释 +1 - 期待“大多数现代 CPU 实现虚拟寻址/虚拟内存”,2014/5 销售的大多数(数十亿)处理器是相对较小的简单嵌入式处理器(大多数是 32 甚至 16位)并且肯定有百万中的100s不使用虚拟寻址。 C在那里很受欢迎。但我同意““大多数 64 位 CPU 实现......”
  • @Chux,你让我明白了,但我可以用一整段来尝试定义我们正在谈论的处理器集..“32 位或 64 位的现代 CPU适用于台式机、笔记本电脑、移动设备、服务器平台”。即使那样,您也可以在该语言中戳洞。关键是您必须了解对话的上下文 - firefox 将在其上运行的 CPU。
  • 很好的解释,涵盖了一些不明显的细节,例如如何在页面中对齐数组。但是 TLB 未命中不会运行内核代码。硬件遍历页表以找到该页的条目。 TLB 是页表的缓存。操作系统只需要在页面不存在于页表中(或在没有所需权限的情况下存在,例如写入)时参与。
  • 显然某些 CPU 架构(例如 MIPS)确实具有软件 TLB 未命中处理,如 @antiduh 所述。无论如何,我编辑了这篇文章以使其更正确,但我可能让它变得不必要的长或更加混乱。我确实添加了一些维基百科链接,并将示例中的页面大小更正为标准 4kiB。
【解决方案2】:

他们使用的技术类似于 Windows 页面堆调试模式,只是不是将每个 VirtualAlloc() 粘贴在其自己的虚拟内存页面中的堆,而是粘贴每个数组(静态或基于堆栈)的系统在它自己的虚拟内存页面中(更准确地说,它将分配放在页面的 end 中,因为在数组的末尾运行远比尝试在它的开头之前访问更常见);然后它会在分配页面之后放置一个不可访问的“保护页面”,甚至在它们的情况下放置大量页面。

这样,边界检查就不是问题了,因为越界访问将触发访问冲突 (SIGSEGV) 而不是破坏内存。这在早期的硬件上是不可能的,因为 32 位机器只有 100 万页可以玩,这不足以处理非玩具应用程序。

【讨论】:

  • 那不是占用更多内存吗?假设他们使用 4K 页面,对于小于 4K 的小数组,这将使用更多内存。如果他们使用2M页面或1G页面,那真的很浪费。
  • @MarkLakata 在我引用的论文中,他们正在解决内存使用稀疏的问题。
  • @MarkLakata -- 它使用大量的虚拟地址空间 -- ofc,只有存储东西实际需要的物理存储被消耗,因为保护/陷阱页面没有根本不需要任何东西的支持。
  • 但是虚拟内存/物理内存映射是以页面大小为单位完成的(默认为 4K)。您必须将整个虚拟内存页面映射到整个物理内存页面。所以这意味着一个长度为 32 字节的小数组(例如)现在将占用 4096 字节。作者承认 TLB 和缓存性能也会受到影响,但我想这在他们的基准测试中是可以接受的,因为他们所有的阵列都远大于 4K。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-25
  • 1970-01-01
  • 2019-09-24
  • 2012-04-27
  • 2016-07-22
  • 1970-01-01
相关资源
最近更新 更多