现代微处理器缓存的简史
现代计算机微处理器是复杂的机器,为了榨干处理器的每一分算力,CPU做了很多的优化架构和设计。最早的计算机是相对简单的机器。他们按照既定的顺序执行用户提供的程序,精确地遵循每个二进制编码指令,提供可预测的程序流程和结果。这在计算的早期阶段运行良好,很大程度上是因为计算机的不同部分以相似的速度运行。例如,处理器(CPU)并不比连接到它的存储器芯片快很多。这样,处理器只需简单地从程序需要的内存加载数据。
随着时间的推移,计算机不同组件的相对性能发生了翻天覆地的变化。微处理器在频率方面以莫斯定理的速度飞速增加,从每秒处理几千条指令到数百万条,最终每秒数十亿条指令。与此同时,可以安装在单个芯片上的晶体管数量(面积)也几何数的增加,从数千个增加到今天高端处理器上的数十亿个晶体管。这需要越来越复杂的设计。然而,虽然计算机的其他部分也在高速发展(内存芯片从MB到几十GB),但相对远远的落后于处理器的发展。由于这种性能差异导致的直接后果就是外部存储器吞吐量成为了系统处理的瓶颈,处理器经常要等待数百,数千或数百万的数据处理"周期"。
为了解决这个瓶颈,业界在采用了在处理中增加高速缓存的架构。高速缓存中存储了最近使用的数据的拷贝,更接近处理器的功能单元(执行计算的部分)。数据值存储在较慢的外部存储器中,当它们由程序第一次到加载时,会在在处理器高速缓存中拷贝一份作为缓存下来。实际上,现代处理器(几乎)从不直接对存储在外部存储器中的数据进行操作,都是直接从缓存中读取数据。处理器的高速缓存也做了多层架构,其中最高级别(L1)最接近处理器的"核心",而较低级别(L2,L3等)则离的更远些。每一级的存储都有不同的功能和大小。

在数字电子产品中,人们常说你可以"小而快"或"大而慢",但很少能同时具有这两种特征。现代处理器中的L1数据高速缓存仅为32kB,而靠近外部存储器的L3(有时称为LLC,最后一级高速缓存)可能是32MB甚至更高。高速缓存的组成根据需求而定,一般来说都遵循包容性设计,即相同数据的拷贝将包含在高速缓存的多个级别内,具体取决于上最近一次使用的时间。处理器内的替换算法将清理L1高速缓存的较少使用的条目,使其在保持L2或L3内的拷贝的同时加载新数据。因此,最近使用的数据必须从内存中加载是很少见的,但是在需要时它可能会从更大,更慢,更低级别的缓存(L2,L3)中调入到L1。

缓存是多个单独的计算核心,线程和程序之间的共享资源。现代处理器芯片往往包含多个内核。其中每个内核都像传统的单处理器计算机一样。每个程序可以执行单个程序或运行同一程序的多个线程,并与共享内存一起运行。每个核心都有自己的L1缓存,也可能有自己的L2缓存,而较大的L3通常被处理器中的所有内核共享。然后,处理器采用称为高速缓存"一致性"的技术,使每个核心的存储器位置的内部拷贝与存储在另一个核心的高速缓存中的拷贝做同步。这是通过内核跟踪内存的所有权并在每次更新内存时在内部消息机制来实现。
缓存的共享可以极大地提高性能、方便处理,但是这也一种潜在的漏洞根源。之前爆发的 "幽灵"和"熔断"漏洞就是利用了缓存作为潜在的"旁路"漏洞,通过这些漏洞获取系统的信息。在旁路分析中,数据不是直接访问的,而是由于观察系统的某些属性而推断得出的。在高速缓存的情况下,高速缓存存储器之间的性能的相对差异,以及比处理器慢得多的内存是使用高速缓存架构的初衷开。不幸的是,可以通过测量访问时间的相对差异来利确定一个存储器位置是否位于高速缓存中。如果它包含在缓存中,则最近由于某些其他处理器活动而访问它。作为示例,处理器在推测性执行期间加载的数据将改变高速缓存状态。
虚拟和物理内存寻址
现代处理器高速缓存通常使用虚拟和物理存储器地址的组合来引用数据。物理地址用于访问主存储器。从概念上讲,你可以将计算机中的外部存储器DIMM或DDR芯片视为从地址零开始的一个巨大的值数组,并继续向上直到内存耗尽。物理内存的数量因机器而异,从典型笔记本电脑的8GB到现代高端服务器机器中的数百GB甚至更高。在早期,程序员必须明确跟踪每个物理内存位置中包含的内容,并避免与使用内存的其他应用程序发生冲突。
现代的OS都使用虚拟内存的架构。虚拟内存意味着操作系统能够为每个应用程序提供其自己视图的对外隔离的内存存储,对位置和大小等没有限制也无需关注。每次程序需要访问内存时,地址由称为存储器管理单元(MMU)的处理器内的特殊硬件转换。 MMU与操作系统(OS)协同工作,操作系统创建和管理一组页面表,将虚拟内存地址转换为物理地址。物理内存被分成很小的块,称为页面,大小通常为4KB。页表包含这些页面的索引,这样一个4KB大小的虚拟地址范围将转换为4KB的物理范围。作为进一步的优化,页表本质上是分层的,其中单个地址通过几个表的"遍历"序列被解码,直到它被完全翻译。

MMU包含可以读取甚至更新操作系统管理的页表的特殊硬件。这些包括遍历表格遍历过程的页面表"walker",以及可以更新页表条目以指示最近访问的数据的其他硬件。操作系统使用后者跟踪哪些数据可以暂时"分页"或"交换"到磁盘(如果最近没有使用过)。页表条目可以标记为"不存在",这意味着任何访问相关地址的尝试都会触发称为"页面错误"的特殊条件,该信号指示操作系统采取行动。这样,操作系统可以拦截尝试访问先前已被换出到磁盘的数据,将其数据从新读取到内存中,恢复应用程序访问。
从性能角度来看,页表遍历可能很耗时。操作系统管理的页表存在于真实的物理内存中,读取时必须将其加载到处理器中。遍历一个页表可能需要很多这样的内存访问,如果每次都发生这种访问会非常慢。因此,处理器不会每次都从内存读取,而是将这些表遍历的结果缓存在单独的处理器结构中,称为转换后备缓冲区(TLB)。因此,最近使用的转换可以更快地解析为物理地址,因为处理器只需要搜索TLB。如果条目不存在,则处理器将执行更慢的内存页面遍历并更新TLB条目。这就是最近TLBleed漏洞的另一种针对TLB的无关攻击的利用前提。
当程序读取或写入存储器时,首先访问通过最高级别(L1)数据高速缓存,次级缓存也叫虚拟索引,物理标记(VIPT)。这意味着缓存使用部分虚拟和部分物理地址来查找内存位置。当检查负载的虚拟地址时,处理器将同时搜索TLB以进行虚拟到物理页面转换,同时开始通过使用单个页面内的偏移来搜索高速缓存以寻找可能的匹配条目。因此,在几乎每个现代处理器中,从虚拟存储器位置读取的过程都非常复杂。这种常见的设计优化解释了为什么L1数据高速缓存在具有4KB页面大小的处理器上通常为32KB,因为它们受到单个页面中可用的偏移位数的限制,开始高速缓存搜索的。
英特尔处理器则进一步优化了它们如何处理页表遍历和"终端故障"(不存在)的过程。在首先回顾现代处理器的随机性质后,我们将进一步深入研究。如果你已经熟悉"熔断"漏洞的原理,可以跳过下一节。
乱序和推测执行
与现代计算机平台的其他部分相比,采用高速缓存实现了更快的微处理器性能改进。学术界和工业界共同努力创造了基础架构的创新,例如乱序(OoO)和推测执行。随着晶体管数量的增加和处理器变得越来越复杂,可能的优化进一步推进,但它们仍然建立在OoO架构基础之上。
在OoO中,处理器在概念上分为"有序"前端和"无序"后端。前端将用户程序作为输入。该程序本质上是顺序执行的,基于数据值的条件判断,由代码块和偶尔的分支(例如"if"语句)跳转到其他块。有序前端将程序中包含的指令发送到无序后端。当调度这些指令进行调度时,在称为重新排序缓冲器(ROB)的处理器结构内分配条目。 ROB实现了数据依赖性跟踪,实现了OoO的关键创新,只要程序员最终无法区分,指令就可以按任何顺序执行。它们只能看到与顺序执行架构相同的效果。
ROB有效地用于将有序机器转换成所谓的"数据流"机器,其中相关指令被迫等待执行直到它们的输入值准备好。每当包含程序指令的ROB中的条目具有所有可用的相关数据值(例如从存储器加载)时,它将被发布给处理器功能单元,并且结果存储回ROB中以供之后指令使用。随着ROB中的条目老化(变成"机器中最老的指令"),它们被称为已"退休"并且可供程序员可见的机器视图使用。这被称为"架构可见"状态,它与从程序的顺序执行获得的状态相同。以这种方式重新排序指令提供了显着的加速。我们以下面一个伪代码为示例:
LOAD R1
LOAD R2
R3 = R1 + R2
R4 = 2
R5 = R4 + 1
R6 = R3 + 1
示例使用字母R来指定称为寄存器的处理器内部存储器位置,或更广泛地指定为GPR(通用寄存器)。通常只有少量寄存器,在Intel x86-64机器的情况下为16。为方便起见,我们将它们编号为R1,R2等,而实际上,它们还有其他名称,例如RAX,RBX等。
在经典的顺序执行架构中,前两个指令可能导致机器等待("停止"),同时访问慢速外部存储器。缓存会加快速度,但即使这两个值包含在处理器缓存的某个级别内,加载指令完成时仍可能会有一个小的延迟。通过OoO机制处理器将跳过这两台指令,虽然指令3取决于前两个指令(具有"数据依赖性"),但指令4和5是独立的。实际上,这两条指令完全不依赖于先前的指令。它们可以随时执行,甚至不依赖于外部存储器。他们的结果将存储在ROB中,直到早先的指令也完成为止,整个程序"退休"。
上一例中的指令编号6称为数据依赖性。就像指令3如何依赖于加载存储器位置L1和L2的结果一样,指令6取决于添加这两个存储器位置的结果。现实中的程序往往有很多这样的依赖项,都通过在ROB中跟踪。
推测建立在OoO执行的基础之上。在推测执行中,处理器再次以与编写程序的顺序不同的顺序执行指令,但是它也推测程序代码中的分支之外。
考虑一个程序语句,例如:
if(it_is_raining)
pack_umbrella();
值"it_is_raining"可能包含在(较慢的)外部存储器中。因此,处理器能够在等待分支条件"解决"的同时继续执行其他有用的工作将是必要的。随机处理器将基于历史猜测(预测)分支的方向,而不是中断(如在经典的,更简单的设计中)。处理器将继续执行分支之后的指令,但它将在ROB中标记结果以指示它们是推测性的并且可能需要被丢弃。处理器内的检查点概念允许推测快速撤消而不会对应用可见,但是一些推测活动的也是有迹可循的。

在幽灵(Spectre)漏洞中,处理器分支预测器可以被欺骗(训练)为预测某种方式。然后,可以推测性地执行代码并且对处理器高速缓存具有可观察的影响。如果我们可以在现有代码中找到合适的"小工具",我们可能会导致故意推测执行通常不应该成为程序流一部分的代码,例如超出数组的范围(在Spectre-v1中),然后导致相关指令执行它将改变缓存中的位置,通过它我们可以推断出我们不应该访问的数据的值
英特尔终端故障漏洞
由于推测处理已经被广泛使用,因此像英特尔这样的处理器供应商做进一步扩展,以便推测处理器内的所有其他可能状态。这包括在虚拟到物理存储器地址的转换期间推测MMU页面遍历器的页表遍历的结果。

英特尔将术语L1 Terminal Fault (终端故障)定义为在页表遍历期间发现页表条目(PTE)"不存在"时出现的情况。这通常是因为操作系统已将页面置换到磁盘(或尚未要求加载它)并将页面标记为不存在以便触发稍后的访问错误。如前所述,交换错觉允许操作系统提供比机器内物理内存更多的虚拟内存。在不存在页面的情况下将发生页面错误,然后操作系统可以确定需要从磁盘交换回的内存位置。
操作系统通过使用"不存在"PTE的位来存储各种内务处理数据,例如包含页面内容的磁盘上的物理位置。英特尔软件开发人员手册(SDM)规定,以这种方式标记为不存在的页面将忽略所有剩余的位(除当前位之外),以便操作系统可以使用它们。 Linux,Windows和其他操作系统都将这些PTE位大量用于"不存在"页面,以支持交换以及各种其他允许的目的。
如前所述,英特尔等处理器使用通用优化,其中虚拟地址转换与对虚拟索引,物理标记(VIPT)L1数据高速缓存的高速缓存访问并行执行。作为进一步优化,英特尔意识到在最佳情况(关键逻辑路径)下,从内存加载的数据值存在于缓存中,并且存在有效的页表转换。换句话说,页表不太可能具有标记为"不存在"的条目。因此,现代英特尔处理器会稍微延迟处理对当前位检查,并将页表条目(PTE)的内容直接转发到缓存控制逻辑,同时执行所有其他检查,包括条目是否有效。猜测漏洞与之前的熔断(Meltdown)漏洞一样,ROB被标记为指示是否应该引发"不存在"故障,但同时,处理器将继续在程序中进一步推测,直到该故障生效。在这个小窗口期间,"不存在"页面的L1数据高速缓存中存在的任何数据值仍将被转发到相关指令。如果可以为地址创建(或导致创建)"不存在"页表条目,并且如果该物理地址当前存在于L1中,则可以使用与熔断(Meltdown)类似的攻击来从物理地址读取数据数据缓存。这被称为L1终端故障攻击(L1 Terminal Fault attack)。
在Linux上,攻击者可以利用此漏洞并尝试通过恶意使用mprotec()系统调用来破坏内核或其他应用程序,从而为可能存在于其中的感兴趣的物理地址导致"不存在"的页表条目缓存。如果他们可以欺骗其他应用程序(或内核)加载特定敏感的信息:例如加密密钥,密码或其他敏感数据。 则可以使用与熔断漏洞利用代码类似的攻击来提取它。可以通过改变Linux生成"不存在"PTE的方式来减轻这种攻击,使得某些物理地址位总是在PTE中设置(使用廉价的屏蔽操作),因此处理器仍将转发"不存在"中的物理地址。但是,除了最极端的情况之外,它将是一个超出填充物理内存范围的超大的物理地址。
故障影响
针对单机的L1TF攻击可以通过几行内核代码(所有漏洞涉及版本,并且还被提交包含在上游Linux中)来缓解。此缓解措施机会不会有性能影响,需要及时升级系统。
不幸的是,此漏洞还有其他几个组件。
一个涉及Software Guard Extensions(SGX)。 SGX是一种英特尔技术,也被称为"安全飞地",用户可以在其中提供安全的软件代码,这些代码将在一个特殊的受保护"飞地"中运行,这将使该软件甚至不被操作系统观察到。 SGX的典型用例是为权限管理,加密和其他软件提供篡改保护。 Red Hat不提供SGX软件,该软件通常由第三方直接拥有和管理。英特尔通过发布处理器微码更新来保护SGX,该更新旨在防止它通过L1TF漏洞的"Foreshadow"SGX特定变体受到损害。
L1TF的另一个变体涉及虚拟化用例。在虚拟化部署中,英特尔处理器采用称为EPT(扩展页表)的技术,其中页表由管理程序,在该管理程序下运行的客户操作系统和硬件共同管理。 EPT取代了一种较旧的纯软件方法,即虚拟机管理程序被迫使用影子页面表。在较旧的设计中,每次客户操作系统想要更新其自己的页表时,管理程序都必须暂停客户机,更新自己的影子表(由真实硬件使用),然后恢复客户机。这对于确保guset永远不会尝试创建访问虚拟机管理程序不允许的内存的页表是必要的。
EPT显著提高了性能,因为客户机操作系统可以像在裸机上一样管理自己的页表。在EPT下,每次内存访问都被多次翻译,首先使用来客户机页表从客户虚拟地址到客户物理地址,然后通过虚拟机管理程序页面表从客户物理地址到主机物理地址。此过程允许本机裸机硬件辅助页面转换的所有好处,同时仍允许管理程序保持控制,因为它可以安排发生各种暂停,并管理哪些客户物理内存可用。
当英特尔实施EPT时,它是现有内存页面架构的扩展。翻译的额外阶段似乎可以简单地添加到其他步骤之后。这样处理很简单方便,但是有一个小问题。在客户虚拟到物理地址转换中出现的终端故障情况可导致未翻译的客户物理地址被当做主机物理地址,并被读入L1数据高速缓存。因此,恶意客户端程序可能创建标记为"不存在"的EPT页表条目,并且还包含它想要从中读取的主机物理地址。如果该主机物理地址在L1数据高速缓存中,则它可以直接读取它。
因此,如果恶意客户机应用可以导致虚拟机管理程序(或其他客户机)将秘密加载到L1数据高速缓存中(例如,仅通过在其正常操作期间使用数据值),它可以使用攻击来提取该数据,类似于Meltdown。
L1TF是虚拟化架构的重大威胁,尤其是那些包含公有和私有虚拟机混合的环境中。幸运的是,L1TF漏洞可以通过适度的系统性能成本得到缓解。
该漏洞利用需要将数据缓存在被攻击的主机的L1数据高速缓存中,对于攻击者感兴趣的信息,可以在返回到客户虚拟机之前,对L1进行刷新。执行此刷新并非没有成本,但是从L2到L1缓存的重新填充很快(仅几百个周期)并且使用这些技术的处理器中存在的高带宽内部总线。因此,开销大约为百分之几。
只要在受影响的计算机上使用虚拟化,就会自动启用L1数据缓存刷新缓冲。
并发多线程(超线程)
处理器可以实现称为并发多线程(SMT)的优化。 SMT由Susan Eggers发明,她今年早些时候因为她对该领域的贡献而获得了着名的Eckert-Mauchly奖。 Eggers在20世纪90年代意识到,通过将单个物理处理器内核分成几个较轻的线程,可以实现更高的程序线程级并行性。 SMT不是复制完整核心的所有资源,而是仅复制使两个单独的执行线程同时运行所需的更重要的资源。这个想法是,在同一程序的两个线程之间可以紧密地共享昂贵的(就晶体管数量而言)高速缓存等资源,因为它们通常对相同的数据上运行。这些线程实际上用于将有用数据提取到其共享缓存中,而不是破坏性地竞争,例如在"生产者 - 消费者"情况下,其中一个线程生成另一个线程使用的数据。
英特尔是SMT的早期商业采用者之一。它们的实现,称为"超线程"已经生效,导致普通计算机用户将其计算机中的核心和线程数量作为关键特性。在超线程中,单个核心中存在两个对等线程(兄弟)。每个都是动态调度的,以便使用核心的可用资源,以便为在共享数据上运行的真正线程应用程序实现高达30%的吞吐量提高。同时,努力减少不紧密共享资源的两个线程之间的无意破坏性争夺(例如,在高速缓存占用方面)的影响。
实际上,英特尔超线程的一般情况下非常好,在许多情况下,最终用户很难区分超线程线程和其他物理内核。在Linux下,这些线程"逻辑处理器"信息(在/ proc /cpuinfo中)几乎与完整处理器核心相同。两者不同的唯一区分方法是查看关联的信息,如输出的"core id","cpu cores"和"siblings"字段的信息,或者解析此命令的更结构化的命令输出,例如"lscpu"。当然,Linux调度程序知道不同之处,并且会尝试不将不相关的线程安排到同一个内核上。尽管如此,在还有许多情况下(例如在HPC应用),不相关的线程之间的竞争可能超过性能提高。
在这些情况下,某些用户通过BIOS设置禁用超线程。
不跨越不同工作负载拆分超线程线程的概念长期以来一直延伸到虚拟化领域。出于多种原因,长期有意义仅将完整核心(所谓的"核心调度")分配给虚拟机实例。共享单个核心的两个不同虚拟机在分配底层高速缓存和其他资源时可能会干扰彼此的性能。然而,对于所有可能出现的潜在问题,长期以来一直很容易将线程视为廉价的额外内核。因此,现代虚拟机机的部署中,在同一核心的超线程之间拆分VM是很常见的,而OpenStack等技术通常会默认执行此操作。
虽然这从来都不是一个好主意,但在存在L1TF漏洞的情况下,对整体安全性的影响更更加突出。超线程同时运行("SMT"中的"S"),因此,一个线程可能正在运行管理程序代码或另一个虚拟机实例,同时对等线程的客户机执行恶意程序。当在对等线程上进入恶意客户端时,将刷新L1数据高速缓存,但遗憾的是,不可能阻止它随后观察其对等线程执行的高速缓存加载。因此,如果两个不同的虚拟机同时在同一个核心上运行,则很难保证它们不能相互执行L1TF攻击并窃取机密。
L1TF对超线程的精确影响取决于具体的使用案例和所使用的虚拟化环境。在某些情况下,公共云供应商(通常构建专用硬件以协助隔离)可能会采取措施使超线程安全。在其他情况下,例如在具有公有虚拟机的传统企业环境中,可能需要禁用Intel超线程。由于这种情况因用例而异,从一个环境到另一个环境。
添加新手交流群:币种分析、每日早晚盘分析
添加助理微信,一对一亲自指导:YoYo8abc