Bounds检查64位硬件

Rag*_*thy 18 c c++ x86-64 sandbox virtual-memory

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

作者说:

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

我试图了解64位硬件如何对C/C++进行自动边界检查(假设编译器支持硬件).我在SO中找不到任何答案.我找到了一篇关于这个主题的技术论文,但我无法理解这是怎么做到的.

有人可以在边界检查中解释64位硬件辅助吗?

ant*_*duh 11

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

所以我的程序访问地址0x8050,它位于虚拟页面8中(假设标准的4096字节(0x1000)页面大小).CPU看到虚拟页面8被映射到物理页面200,因此在物理地址处执行读取200 * 4096 + 0x50 == 0xC8050.(正如TLB缓存页表查找一样,更熟悉的L1/L2/L3缓存对物理RAM的缓存访问.)

当CPU没有该虚拟地址的TLB映射时会发生什么?这种情况经常发生,因为TLB的大小有限.答案是CPU生成页面错误,由OS处理.

页面错误可能会导致多种结果:

  • 一,操作系统可以说"噢,它只是不在TLB,因为我无法适应它".操作系统使用进程的页表映射从TLB中删除一个条目并填入新条目,然后让进程继续运行.在中等负载的机器上每秒发生数千次.(在具有硬件TLB未命中处理的CPU上,如x86,这种情况在硬件中处理,甚至不是"次要"页面错误.)
  • 二,操作系统可以说"噢,现在虚拟页面没有映射,因为它使用的物理页面被交换到了磁盘,因为我的内存不足".操作系统暂停进程,找到要使用的内存(可能通过交换其他一些虚拟映射),为请求的物理内存排队磁盘读取,并在磁盘读取完成时,使用新填充的页表映射恢复进程.(这是一个"主要"页面错误.)
  • 三,进程正在尝试访问没有映射的内存 - 它不应该读取内存.这通常称为分段错误.

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

我们可以利用这个优势来执行"硬件加速"索引检查,但是我们尝试这样做时会遇到更多绊脚石.

首先,一般的想法:对于每个数组,我们将它放在自己的虚拟内存区域中,所有包含数组数据的页面都照常映射.在实际数组数据的任一侧,我们创建了不可读和不可写的虚拟页面映射.如果您尝试在阵列外部读取,则会生成页面错误.编译器在创建程序时插入自己的页面错误处理程序,它处理页面错误,将其转换为索引越界异常.

第一个绊脚石是我们只能将整个页面标记为可读或不可读.数组大小可能不是页面大小的偶数倍,因此我们遇到问题 - 我们无法在数组结束之前和之后准确地放置数据块.我们能做的最好的事情是在数组开始之前或数组与最近的'fence'页面之间的数组结束之后留下一个小间隙.

他们如何解决这个问题?那么,在Java的情况下,编译执行负索引的代码并不容易; 如果确实如此,那么无论如何都是无关紧要的,因为负索引被视为无符号,这使得索引远远超出数组的开头,这意味着它很可能会击中未映射的内存,并且无论如何都会导致错误.

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

-----------++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
|  Page 1  |  Page 2  |  Page 3  |  Page 4  |  Page 5  |  Page 6  |  Page 7  | ...
                 |----------------array---------------------------|
Run Code Online (Sandbox Code Playgroud)

现在,如果索引是数组的末尾,它将触及第7页,这是未映射的,这将导致页面错误,这将变成索引超出范围的异常.如果索引在数组的开头之前(也就是说,它是负数),那么因为它被视为无符号值,它将变得非常大且为正,使我们再次超过第7页,导致未映射的内存读取,从而导致页面错误,它将再次变成索引超出范围的异常.

第二个绊倒的是我们在映射下一个对象之前应该留下大量未映射的虚拟内存超过数组的末尾,否则,如果索引超出范围,但是远远超出界限,它可能会命中一个有效页面并且不会导致索引越界异常,而是会读取或写入任意内存.

为了解决这个问题,我们只使用了大量的虚拟内存 - 我们将每个数组放入其自己的4 GiB内存区域,其中只有前N个页面实际映射.我们可以这样做,因为我们只是在这里使用地址空间,而不是实际的物理内存.64位进程有大约40亿个4 GiB区域的内存块,因此在用完之前我们有足够的地址空间可供使用.在32位CPU或进程上,我们只能使用很少的地址空间,因此这种技术不太可行.事实上,许多32位程序现在正在耗尽虚拟地址空间,只是试图访问实际内存,从未尝试在该空间中映射空的"fence"页面以尝试用作"硬件加速"索引范围检查.