访问内核地址空间的时候,只要创建了虚拟地址的映射(即可以通过页表翻译出一个有效的物理地址),CPU都可以访问这些地址的内容。和访问用户地址空间唯一不同是会进行权限检查,由于当前CPU权限级别不够而访问内核空间地址的时候会触发异常。因此,用户空间不能简单地通过读取内核地址的内容来获得秘密数据。然而,乱序执行的特性允许CPU在一个很小的时间窗口内(从执行了非法内存访问的指令到触发异常),仍然会继续执行指令。Meltdown就是利用了乱序执行的特性完成了。
在代码列表中的第4行,我们访问了位于内核地址空间的memory(地址保存在RCX寄存器),获取了一个字节的数据,保存在AL寄存器(即RAX寄存器的8个LSB比特)。根据2.1节中的描述,MOV指令由CPU core取指,解码成μOPS,分配并发送到重排序缓冲区。在那里,architecturalregister(软件可见的寄存器,例如RAX和RCX)会被映射成底层的物理寄存器以便实现乱序执行。为了尽可能地利用流水线的代码)已经解码并分配为uOPs。该uOPs会进一步送到保留站(暂存uOPs),在保留站中,uOPs会等待相应的执行单元空闲,如果执行单元准备好,该uOPs会立刻执行,如果执行单元已经达到了容量的上限(例如有3个加,那么可以同时进行3个加法运算,第四个加法uOPs就需要等待了)或uOPs操作数值尚未计算出来,uOPs则被延迟执行。
当在程序第4行加载内核地址到寄存器的时候,由于乱序执行,很可能CPU已经把后续指令发射出去,并且它们相应的μOPs会在保留站中等待内核地址的内容到来。一旦在公共数据总线上观察到所获取的内核地址数据,这些μOPs就会立刻开始执行。
当μOPs执行完毕后,它们就按顺序进行retire(这个术语叫做retire,很难翻译,这里就不翻译了,但是和commit是一个意思),因此,μOPs的结果会被提交并体现在体系结构状态上。在提交过程中,在执行指令期间发生的任何中断和异常都会被处理。因此,在提交MOV指令的时候发现该指令操作的是内核地址,这时候会触发异常。这时候CPU流水线会执行flush操作,由于乱序执行而提前执行的那些指令(Mov指令之后)结果会被清掉。然而,在触发这个异常和我们执行的步骤2之间有一个竞争条件(race condition),我们在下面描述。
根据Gruss等人的研究[ 9 ],预取内核地址有时成功。我们发现:预取内核地址可以略微改善某些系统的性能。
在步骤1中乱序执行的指令序列能否成为瞬态指令序列是需要条件的。如果的确是瞬态指令序列,那么它必须要在MOV指令retirement之前被执行(即在触发异常之前),而且瞬态指令序列会基于秘密数据进行计算,而这个计算的副作用可以用来向者传递秘密数据。