一切还是要从CPU指令执行的框架——流水线说起。Intel当然不至于明知你要用一个用户态的进程读取Kernel内存还会给你许可。但现代CPU流水线的设计,尤其是和性能优化相关的流水线的特性,让这一切充满了变数。
给所有还没有看过云杉网络连载的系列文章《x86高性能编程笺注系列》的读者一点背景知识的介绍:
x86 CPU为了优化性能,在处理器架构方面做了很多努力。诸如“多级缓存”这一类的特性,是大家都比较熟悉的概念。还有一些特性,比如分支预测和乱序执行,也都是一些可以从并行性等方面有效提升程序性能的特性,并且它们也都是组成流水线的几个关键环节。即便你暂时还不能准确理解其含义,但望文生义,也能看出来这肯定是两个熵增的过程。熵增带来无序,无序就会带来更多漏洞。
分支预测是一种提高流水线执行效率的手段。在遇到if..else..这种程序执行的分支时,可以通过以往的历史记录判断哪一分支是最可能被执行的分支,并在分支判断条件真正返回判断结果之前提前执行分支的代码。详情可以在上面提到的连载文章中阅读。
需要强调的是,提前执行的分支代码,即便事后证明不是正确的分支,其执行过程中所读取的数据也可以进入L1缓存。在Intel的官网文档《Intel® 64 and IA-32 Architectures Optimization Reference Manual》第2.3.5.2节中指:
L1 DCache Loads:
– Be carried out speculatively, before preceding branches are resolved.
– Take cache misses out of order and in an overlapped manner.
Show you the [伪] code:
if (likely(A < B)) { value = *(kernel_address_pointer);}
当分支判断条件A < B被预测为真时,CPU会去提前执行对内核地址的读取。当实际条件为A > B时,虽然内核的值不会真正写入寄存器(没有retire),但会存入L1 Cache,再加之上一节介绍的,获取L1 Cache的值毋须地址转换,毋须权限检查,这就为内核信息的泄漏创造了可能。
从理论上来讲,如果可以控制程序的分支判断,并且可以获取L1缓存中的数据(这个没有直接方法,但可以通过其他间接手法)的话,就完全可以获取内核信息。而分支预测这种特性是不能随随便便就关闭的,这也就是这次问题会如此棘手的原因。
云杉网络
January 5, 2018
技术干货