根据前面的描述,在instruction commit阶段,当检测到用户态访问内核地址的时候,除了触发异常,CPU还会清除指令的操作结果,也就是说AL寄存器会被清零。如果瞬态指令序列在和异常的竞争中失败了(寄存器清零早于程序列表中第代码执行),那么很可能从内核地址读出的并非其真是值,而是清零后的数值。对寄存器清零也是合理的,因为如果异常没有被处理,用户空间的应用程序会终止,该进程的core dump文件中会保留寄存器的内容,如果不清零,那么内核空间的数据可以通过core dump文件泄露出去。清零可以修正这个issue,内核空间数据的安全。为了防止瞬态指令序列继续操作错误的“0”值,Meltdown会重读地址直到读出非“0”值(第6行代码)。
你可能会问:如果秘密数据就是0怎么办?其实当异常触发后,瞬态指令序列终止执行,如果秘密数据确实等于0,则不存在任何cacheline被加载。因此,meltdown在进行探测数据cacheline扫描过程中,如果没有任何cacheline命中,那么秘密数据实际上就是“0”。
无论是读出数值非“0”或无效地址访问触发了异常,代码中的循环逻辑都会终止。注意,这个循环不会降低的性能,因为,在两种情况中,CPU会提前允许非法内存访问指令之后的代码指令,而CPU并不关心这些指令是一个循环控制或是一个线性的控制流。无论哪一种情况,从异常处理函数(或者异常)返回的时间都是一样的,和有没有循环控制是无关的。因此,尽早发现读出值是“0”,也就是说尽早发现自己在和异常的竞争中失败并恢复,可以大大提高了读取速度。
在第5.1节的描述中,者通过隐蔽通道一次可以传输8个bit,接收端执行2^8=256次Flush+Reload命令来恢复秘密数据。不过,我们需要在运行更多的瞬态指令序列和执行更多的Flush+Reload测量之间进行平衡。者可以通过隐蔽通道在一次传输中发送任意比特的数据,当然这需要使用MOV指令去读取更多bit的秘密数据。此外,者可以在瞬态指令序列中增加mask的操作(这样可以传送更少的bit,从而减少接收端Flush+Reload的次数)。我们发现在瞬态指令序列中增加的指令数对的性能影响是微不足道的。
描述的meltdown中的性能瓶颈主要是在通过Flush+Reload恢复秘密数据上所花费的时间。实际上在本章中的meltdown代码实现中,几乎所有的时间都将花费在Flush+Reload上了。如果只发送一个bit,那么除了一次Flush+Reload测量时间,其他的我们都可以忽略。在这种情况下,我们只需要检测一个cacheline的状态,如果cache hit,那么传输的bit是“1”,如果cache miss,那么传输的bit是“0”。