1.2 x86和非x86体系结构基础
Intel的x86体系结构是世界上最流行的处理器架构,从1978年8086/8088处理器问世到现在的Core 2 Duo和Core 2 Quad,以及Xeon 5300和7300系列处理器,Intel x86体系结构已经在CPU领域叱咤30多年。
本节将对x86和非x86的体系结构进行简要介绍,由于篇幅所限,无法对所有的知识都深入讲解,但会尽力将它们的思想阐述清楚。
1.2.1 x86的发展历程
Intel公司是世界上最大的CPU制造厂商,占据了80%以上的CPU市场份额。本节以Intel公司的处理器为例介绍x86的发展历程,如图1-1所示。
图1-1 x86的发展历程
1978年,最早的x86处理器8086初次登场。8086是16位的处理器,这个位数指的是CPU GPR(General-Purpose Register,通用寄存器)的数据宽度为16位。8086处理器拥有16位的寄存器和16位的外部数据总线,使用20位地址寻址(拥有1MB的地址空间)。在20世纪80年代初,IBM选择了8086的衍生产品8088作为IBM PC的处理器。IBM的这一举动给x86带来了巨大的发展机遇,并且帮助它成为行业的标准直到今天。
1982年,Intel公司发布了80286处理器,引入了保护模式的概念。
1985年,Intel公司发布了 x86体系结构下的第一款 32位处理器 80386,并引入了虚拟内存。Intel执行副总裁Patrick Gelsinger认为80386的推出是PC行业的革命性转折点,推动了整个行业的发展。
1989年,80486发布,在80386的基础上,80486采用了5级流水线机制,并且引入了片上一级缓存和能量管理。
由于当时数字不能作为商标,Intel公司改变了产品的命名方法。1993年,第一款奔腾处理器(Pentium)发布,此款处理器在80486的基础上进一步加大一级缓存,并将其分成指令缓存和数据缓存两个部分,进一步加快了处理器对内存的访问速度。同时,奔腾处理器还引入MMX技术,进一步增强了处理器对多媒体处理的支持。
1995年到1999年,Intel公司发布了一系列基于x86体系结构的处理器—奔腾6(简称P6)家族处理器,包括奔腾Pro(Pentium Pro)、奔腾 2(PentiumⅡ)、奔腾 2 至强(Pentium Ⅱ Xeon)、赛扬(Celeron)、奔腾 3(PentiumⅢ)以及奔腾 3 至强(Pentium Ⅲ Xeon)处理器。P6家族处理器采用了超标量(Superscalar)技术,以乱序执行的方式进一步增强了处理器的处理速度。
2002年到2006年,奔腾 4(Pentium 4)家族处理器占据了主导地位。奔腾 4家族的处理器基于NetBurst微处理器结构,在提升性能的同时,进一步增强了对多媒体处理的支持,并且引入了超线程(Hyper-Threading)的概念,引领单处理器的性能走向巅峰。此外,在奔腾 4 的 672 和 662 处理器上,Intel还首次加入了虚拟化支持,即Intel VT技术。也是在这段时期,由于AMD发展的64位扩充技术的刺激,Intel公司开始推出64位版x86。
从2006年开始,处理器进入多核时代(Multicore),Intel相继发布了Core Duo和Core 2 Duo系列处理器。
1.2.2 x86-64
x86-64 是 x86 架构的延伸产品,是一种 64 位微处理器架构及其相应的指令集。
在x86-64出现以前,Intel与惠普联合推出IA-64架构,此架构不与x86兼容,且市场反应冷淡。于是,与 x86 兼容的 x86-64 架构应运而生。1999 年, AMD首次公开64位集为IA-32提供扩展,称为x86-64(后来改名为AMD64)。此架构后来也为Intel所采用,也就是现在的Intel 64。
x86-64能有效地把 x86架构移植到 64位环境,并且兼容原有的 x86应用程序,市场前景广阔。外界使用x84-64或者x64称呼这个64位架构,以保持中立,不偏袒任何一家厂商。
1. AMD 64
AMD 64指令集被应用在Athlon 64、Athlon 64 FX、Athlon 64 X2、Turion 64、Turion 64 X2、Opteron及较新款的Sempron和Phenom处理器上。其主要特点有:支持64位通用寄存器、64位整数及逻辑运算和64位虚拟地址。AMD 64架构相比之前的32位架构有如下重大改进。
● 新增寄存器。
● 地址宽度加长。
● 新增扩充指令集SSE2、SSE3。
● 新增“禁止运行”位(No-Execute,NX bit):此位的设置可以防止蠕虫病毒的缓存溢出攻击。
2. Intel 64
Intel 64指令集被应用于Pentium 4、Pentium D、Pentium Extreme Edition、Celeron D、Xeon、Intel Core 2、Intel Core 2 Quad、Intel Core i3、Intel Core i5及Intel Core i7处理器上。Intel 64架构加入了额外的寄存器和其他改良的指令集,可使处理器直接访问超过4GB的存储器,允许运行更大的应用程序。通过64位的存储器地址上限,其理论存储器容量上限达16 000 000TB(16EB),但大多数操作系统和应用程序在可见的未来都用不到如此巨大的地址空间,在初期的应用上并未支持完整的64位地址。Intel 64架构相比之前的32位架构有如下重大改进。
● 新增寄存器。
● 地址宽度加长。
● 新增改良指令集。
● 新增“禁止运行”位(eXecute Disable,XD bit):相当于AMD 64的NX bit,通过设置此位可以防止蠕虫病毒的缓存溢出攻击。
1.2.3 x86内存架构
硬件架构中最复杂、最核心的部分就是其内存架构。为了使读者理解现代计算机体系架构,本节将对 x86 的内存架构进行讲解,主要内容包括:x86 为操作系统提供了怎样的内存架构,以及操作系统是如何使用它们的。
本节主要以32 位架构为例进行讲解,并在最后添加了一些x86-64架构下的相关内容。
1. 地址空间
地址空间是所有可用资源的集合,我们姑且将它看做一个大大的数组,那么地址就是这个数组的索引。地址空间可以划分为物理地址空间和线性地址空间两大类,具体介绍如下。
(1)物理地址空间
硬件平台通常划分为CPU、内存和其他硬件设备三个部分。其中,CPU是整个硬件平台的主导者,内存和其他硬件设备都是CPU可以使用的资源。这些资源组合在一起,分布在 CPU 的物理地址空间内,CPU 使用物理地址索引这些资源。
物理地址空间的大小由CPU实现的物理地址位数所决定,物理地址位数由CPU经过MMU(Memory Management Unit,内存管理单元)转换后的外地址总线位数决定。外地址总线位数与CPU处理数据的能力(即CPU位数)没有必然的联系,例如,16位的8086 CPU具有20位地址空间。
一个硬件平台只有一个物理地址空间,但每个程序都认为自己独享整个平台的硬件资源。为了让多个程序能够有效地相互隔离,也为了它们能够有效地使用物理地址空间的资源,引入了线性地址空间的概念。
(2)线性地址空间
线性地址空间的大小由CPU实现的线性地址位数所决定,线性地址位数由CPU 的内地址总线位数决定。内地址总线与 CPU 执行单元相连,内地址总线位数往往与CPU位数一致,如果是32位处理器,那么它就实现了32位线性地址,其线性地址空间为4GB。需要注意的是,线性地址空间的大小与物理地址空间的大小没有必然联系,Intel 的PAE平台具有4GB的线性地址空间,而其物理地址空间为64GB。
线性地址空间会被映射到某一部分物理地址空间或整个物理地址空间。一个硬件平台上可以有多个线性地址空间,CPU负责将线性地址空间转换成物理地址空间,保证程序能够正确访问到该线性地址空间所映射到的物理地址空间。在现代操作系统中,每个进程通常都拥有自己的私有线性地址空间。一个典型的线性地址空间构造如图1-2 所示。
图1-2 线性地址空间构造
2. 地址
地址是访问地址空间的索引。根据访问地址空间的不同,索引可以分为物理地址和线性地址。但由于x86特殊的段机制,还存在一种额外的地址—逻辑地址。
(1)逻辑地址
逻辑地址是程序直接使用的地址(x86 无法禁用段机制,逻辑地址一直存在)。逻辑地址由一个16位的段选择符和一个32位的偏移量(32位平台)构成。下面以具体程序为例进行解释。
int a = 100; int *p = &a;
上述语句中的指针变量 p存储的就是变量 a的逻辑地址。实际上,p中存储的仅是逻辑地址的偏移部分,而偏移对应的段选择符位于段寄存器中,并没有在程序中显示。
(2)线性地址
线性地址又称虚拟地址。线性地址是逻辑地址转换后的结果,用于索引线性地址空间。当 CPU 使用分页机制时,还需要将线性地址转换成物理地址才能访问平台内存或其他硬件设备;当分页机制未启用时,线性地址与物理地址相同。
(3)物理地址
物理地址是物理地址空间的索引,是CPU提交到总线用于访问平台内存或其他硬件设备的最终地址,在x86下,物理地址有时也被称为总线地址。
正如(1)、(2)中所阐述的,物理地址与逻辑地址、线性地址的关系总结如下。
● 分段机制启用,分页机制未启用:逻辑地址→线性地址=物理地址
● 分段机制、分页机制同时启用:逻辑地址→线性地址→物理地址
3. x86内存管理机制
x86架构的内存管理机制分为两部分:分段机制和分页机制。
分段机制为程序提供彼此隔离的代码区域、数据区域、栈区域,从而避免了同一个处理器上运行的多个程序互相影响。
分页机制实现了传统的按需分页、虚拟内存机制,可以将程序的执行环境按需映射到物理内存。此外,分页机制还可以用于提供多任务的隔离。
分段机制和分页机制都可以通过配置支持简单的单任务系统、多任务系统或共享内存的多处理器系统。需要强调的一点是,处理器无论在何种运行模式下都不可以禁止分段机制,但是分页机制却是可选选项。下面分别对这两种机制进行详细介绍。
(1)分段机制
分段机制将内存划分成以起始地址(Base)和长度(Limit)描述的块。段可以与程序最基本的元素联系起来,程序可以简单地划分为代码段、数据段和栈,段机制就有相应的代码段、数据段和栈段。
分段机制是x86架构下的朴素内存管理机制,不可以禁用。了解分段机制是了解程序中所使用的逻辑地址转换为CPU内部所使用的线性地址的关键。
分段机制由逻辑地址、段选择符、段描述符和段描述符表4个基本部分构成。当程序使用逻辑地址访问内存的某个部分时,CPU通过逻辑地址中的段选择符索引段描述符表,进而得到该内存对应的段描述符(段描述符描述段的基地址、长度以及读/写、访问权限等属性信息),根据段描述符中的段属性信息检测程序的访问是否合法,如果合法,再根据段描述符中的基地址将逻辑地址转换为线性地址。这个流程可以用图1-3概括。
图1-3 分段机制流程分布
1)段选择符(Segment Selector)。
段选择符是逻辑地址的一个组成部分,用于索引段描述符表以获得该段对应的段描述符。段选择符存放于段选择寄存器中,共16位,结构如图1-4所示。
图1-4 段选择符的结构
各字段的含义如下。
● 索引(Index):段描述符表的索引。
● TI:指明索引哪个段描述符表。当 TI=0 时,表示索引全局段描述符表(Global Descriptor Table,下面简称为GDT);当TI=1时,表示索引本地段描述符表(Local Descriptor Table,下面简称为LDT)。
● RPL:Requested Privilege Level的简称,即所要求的权限级别。RPL存在于段选择寄存器的0、1位,为程序访问段时增加一级检查。
段选择符作为逻辑地址的一部分,对应用程序是可见的。但是,正如前面在逻辑地址中介绍的,应用程序中只存储和使用逻辑地址的偏移部分,段描述符的修改和分配由连接器和加载器完成。
为了使CPU能够快速地获得段选择符,x86架构提供了6个段寄存器存放当前程序中各个段的段选择符。这6个段寄存器分别如下。
● CS(Code-Segment,代码段):存放代码段的段选择符。
● DS(Data-Segment,数据段):存放数据段的段选择符。
● SS(Stack-Segment,栈段):存放栈的段选择符。
● ES、FS、GS:可以存放额外三个数据段的段选择符,由程序自由使用。
由于段选择符是用来索引段描述符表访问段描述符的,为了加速段描述符的访问,x86 在段寄存器后增加了一个程序不可见的段描述符寄存器。当段寄存器被加载到一个新的段选择符后,CPU自动将该段选择符索引的段描述符加载到这个不可见的段描述符寄存器中。这6个段寄存器的构造如图1-5所示。
图1-5 段寄存器的构造
2)段描述符(Segment Descriptor)。
段描述符描述某个段的基地址、长度以及各种属性(例如,读/写属性、访问权限等)。这是分段机制的核心思想。
段描述符的结构如图1-6所示。
图1-6 段描述符的结构
有关段描述符的详细解释,可查阅 Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3A一书中3.4.5节的内容(附下载网址:http://www.intel.com/Assets/PDF/manual/253668.pdf)。在此,我们只详细介绍其中的Base字段、Limit字段和DPL字段。
● Base字段描述了该段的基地址。
● Limit字段描述了该段的长度。
● DPL字段指明描述符权限级别,表示该段所具有的权限。它表示代码访问此段所需要的最低权限。
当CPU通过一个逻辑地址的段选择符获得该段对应的段描述符后,会使用段描述符中各种属性字段对访问进行检查,一旦确认访问合法,CPU将段描述符中的32位基地址和程序中逻辑地址的32位偏移量相加。至此,CPU便获得了逻辑地址所对应的线性地址。
正如前面所讲,x86 在段寄存器后增加了一个程序不可见的段描述符寄存器,每当段寄存器被加载入一个新的段选择符后,CPU自动将该段选择符索引的段描述符加载到这个不可见的段描述符寄存器中。也就是说,CPU只有在更新段寄存器时才索引段描述符表。
3)段描述符表。
在前面介绍段选择符的结构时,我们了解到段选择符中的 TI 字段用于指明索引哪个段描述符表。TI字段的必要性源于x86架构提供了两种段描述符表—GDT和LDT。
系统中至少有一个 GDT 可以被所有的进程访问。与此同时,系统中可以有一个或多个LDT,可以被某个进程私有,也可以被多个进程共享。
● GDT是内存中的一个数据结构。简单地讲,可以将GDT看成是一个数组,由基地址(Base)和长度(Limit)描述。
● LDT 是一个段,需要用一个段描述符来描述。LDT 的段描述符存放在GDT中,当系统中有多个LDT时,GDT中必须有对应数量的段描述符。
为了加速对GDT和LDT的访问,x86架构提供了GDTR寄存器和LDTR寄存器。关于这两种寄存器的具体描述如下。
● GDTR:包括一个32位的基地址(Base)和一个16位的长度(Limit)。
● LDTR:结构与段寄存器相同(同样包含对程序不可见的段描述符寄存器)。
通过使用LGDT/SGDT指令对GDTR进行读取/存储;通过使用LLDT/SLDT对LDTR进行读取/存储。在进行进程切换时,LDTR的值会被换成新进程对应的LDT的段描述符。
通过段选择符索引GDT/LDT的过程如图1-7所示。
图1-7 通过段选择符索引段描述符表
4)分段机制总结。
现在我们已经将分段机制中的关键概念——逻辑地址、段选择符、段描述符和段描述符表全部介绍完毕。下面梳理一下逻辑地址转换为线性地址的过程。
在程序加载阶段,该进程LDT的段选择符索引GDT,获得LDT的段描述符并将其加载到LDTR寄存器中。此外,该进程的CS、DS、SS被加载入相应的段选择符,CPU 根据段选择符的 TI 字段索引相应的段描述符表,获得相应的段描述符,并加载入CS、DS、SS对应的不可见的段描述符寄存器。
程序执行到读/写内存中的数据时,把程序中相应变量的逻辑地址转换为线性地址:①进行必要的属性、访问权限检查;②从 DS 对应的段描述符寄存器获得该段的基地址;③将变量的32位偏移量和段描述符中的基地址相加,获得该变量的线性地址。
(2)分页机制
上面介绍的分段机制将内存划分成以基地址和长度描述的多个段进行管理,对应的逻辑地址以基地址和偏移量来描述。而分页机制是更加粒度化的内存管理机制,使用粒度化的单位“页”来管理线性地址空间和物理地址空间的对应关系。同时,分页机制允许一个页面存放在物理内存中或磁盘的交换区域(如Linux下的Swap分区,Windows下的虚拟内存文件)中,程序可以使用比机器物理内存更大的内存区域,从而使现代操作系统中虚拟内存机制的实现成为可能。
在x86架构下,页的典型大小为4KB,于是一个4GB的虚拟地址空间被划分成1024×1024个页面。物理地址空间的划分与此类似。x86架构允许大于4KB的页面大小(如 2MB、4MB)。由于篇幅所限,此处只对最经典的 4KB 页面管理机制进行介绍。
分页机制的核心思想是通过页表将线性地址转换为物理地址,并配合旁路转换缓冲区(Translation Lookaside Buffer,后面简称为TLB)来加速地址转换的过程。分页机制主要由页表、CR3寄存器和TLB三个部件构成,其流程如图1-8所示。
图1-8 分页机制流程分布
1)页表(Page Table)。
页表是用于将线性地址转换成物理地址的主要数据结构。一个地址对齐到页边界后的值称为页帧号(或者页框架),它实际上就是该地址所在页面的基地址。线性地址对应的页帧号叫做虚拟页帧号(Virtual Frame Number,下面简称为VFN),物理地址对应的页帧号叫做物理页帧号(Physical Frame Number,下面简称为PFN)或机器页帧号。页表实际上是存储VFN到PFN映射的数据结构。
在传统的32位的保护模式中,x86处理器使用两级转换方案,在这种方案中, CR3指向一个4KB的页目录,页目录又分为1024个4KB大小的页表,最后页表分为1024个长为4KB的页。未启用PAE的4KB大小的页面如图1-9所示。
图1-9 未启用PAE的4KB页——二级页表
● 页目录项(Page Directory Entry,下面简称为PDE):包含页表的物理地址。PDE 存放在页目录中,CPU 使用线性地址的 22~31 位索引页目录,以获取该线性地址所对应的PDE。PDE的大小为4B,页目录包含1024个PDE,占用一个4KB的物理页面。
● 页表项(Page Table Entry,下面简称为PTE):包含该线性地址对应的 PFN。PTE 存放在页表中,CPU 使用线性地址的 12~21 位索引页表,以获取该线性地址所对应的 PTE。确定 PFN 后,再将线性地址的0~11位偏移量与其相加,就可以确定该线性地址对应的物理地址。PTE 的大小也是 4B,页表包含 1024 个 PTE,占用一个 4KB 的物理页面。
虚拟内存实现的关键在于PDE和PTE都包含一个P(Present)字段(更多详细的内容可参考 Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3A中的4.3节)。P字段根据其值的不同代表以下两种情况。
当P=1时,物理页面存在于物理内存中,CPU完成地址转换后可以直接访问该页面。
当 P=0 时,物理页面不在物理内存中,当 CPU 访问该页面时,会产生一个缺页错误并交给操作系统的缺页处理程序处理,操作系统通常将存放在磁盘上的页面调入物理内存,使访问可以继续。需要注意的是,由于程序的局部性特点,操作系统会将该页面附近的页面一起调入物理内存,方便CPU的访问。P=0时的PDE和PTE的1~31位都将为操作系统提供物理页面在磁盘上的信息。通常情况下,这些位存储着物理页面在磁盘上的位置。
启用物理地址扩展(之后简称为PAE)后,页表结构将发生相应的变化。页表和页目录的总大小仍是4KB,但页表和页目录中的表项都从32位扩为64位,以使用附加的地址位。这样,页表和页目录都只有512个表项,变成了原来方案的一半,所以又加入了一个级:CR3指向页目录指针表,即一个包含4个页目录指针的表。启用PAE的4KB大小的页面使用的三级页表如图1-10所示。
2)CR3寄存器。
CR3 寄存器也称为页目录基地址寄存器(Page-Directory Base Register, PDBR),存放着页目录的物理地址。一个进程在运行前,必须将其页目录的基地址存入CR3,而且,页目录的基地址必须对齐到4KB页边界。
图1-10 启用PAE的4KB页——三级页表
启用PAE时,CR3指向页目录指针表。
3)TLB。
为了提高地址转换的效率,x86架构使用TLB对最近用到的页面映射进行缓存。TLB中存放着VFN到PFN的转换,当CPU访问某个线性地址时,如果其所在页面的映射存在于TLB中,无须查找页表,即可得到该线性地址对应的PFN,CPU再将它与线性地址的偏移相加,就能得到最后的物理地址。
4)分页机制总结。
将分页机制的三个部件——页表、CR3寄存器和TLB介绍完后,这里再对CPU使用分页机制将线性地址转换成物理地址的过程进行总结。
① CPU访问一个线性地址,在TLB中进行匹配,如果地址转换在TLB中,则跳到步骤⑥。否则,发生了一次TLB Miss(TLB缺失),继续步骤②。
② 查找页表,如果页面在物理内存中,则跳到步骤④。
③ 产生缺页错误,由操作系统的缺页错误处理程序进行以下处理。
● 将页面从磁盘复制到物理内存中。
● 更改对应的PTE,设置P位为1,并对其他字段进行相应的设置。
● 刷新TLB中对应的PTE。
● 从缺页错误处理程序中返回。
④ 此时,页面已经存在于物理内存中,并且页表也已经包含了这个映射。重新在 TLB 中进行匹配,如果地址转换在 TLB 中,则跳到步骤⑥。否则,发生了一次TLB Miss(TLB缺失),继续步骤⑤。
⑤ CPU重新查页表,把对应的映射插入到TLB中。
⑥ 此时,TLB已经包含了该线性地址对应的PFN。将PFN和线性地址中的偏移量相加,就得到了对应的物理地址。
4. x86-64架构
IA-32e是 Intel发布的 64位指令集扩展技术,在此以 IA-32e为例介绍 64位架构下的内存管理机制。
IA-32e模式有如下两个子模式。
● 兼容模式。此模式下,只用了32 位的线性地址,将地址的32~47位全部视为0。
● 64位模式。这种模式产生64位的线性地址,处理器需要保证地址的47~63位完全相同。实际上只使用了48位的地址。
(1)IA-32e的分段机制
分段机制的表现与处理器的工作模式有关。
● 在兼容模式下,分段机制的作用过程与前面介绍的传统的分段机制完全相同。
● 在64位模式下,分段机制的分段功能基本上被禁用,同时提供64位的通用线性地址空间。处理器将CS、DS、ES、SS中的段基址视为0,而FS、GS中存储的段基址可在线性地址的计算过程中使用。FS、GS方便了本地数据的寻址,及操作系统数据结构的确定。这里还有一点需要引起注意,在64位模式下,处理器不会执行段长度检查。
(2)IA-32e的分页机制
64位寻址模式(长模式)下,PAE是必需的,内存页大小可以是4KB、2MB、1GB。与前面介绍的启用PAE时的三级标签页表机制不同,长模式下,系统使用四级标签页表——附加了一个第四级页面映射表(Page-Map Level 4 Table,之后简称为PML 4 Table)。这种机制将48位的线性地址转换为52位的物理地址。下面以 4KB 页面为例对这种机制进行介绍,四级页表的结构如图 1-11所示。
● 第四级页面映射表(PML4):包含页目录指针表的物理基地址、访问权限和内存管理信息。PML4的物理基地址存在CR3中。
● 页目录指针:包含一个页目录表的物理基地址、访问权限和内存管理信息。
● 页目录项:包含一个页表的物理基地址、访问权限和内存管理信息。
● 页表项:包含一个页面的物理地址、访问权限和内存管理信息。
图1-11 四级页表结构
1.2.4 x86-64的基本模式
具有64位扩展技术的处理器可以运行在传统的IA-32模式和IA-32e 模式下,而这些模式也都有自己的子模式,在此对它们进行简要介绍,以方便读者理解x86 CPU的工作。
传统的IA-32模式下,x86有三种运行模式:实模式、保护模式和虚拟8086模式。
● 实模式:是Intel 8086处理器工作的模式。在该模式下,逻辑地址转换后就是物理地址,操作系统或BIOS通常在该模式下准备必要的数据结构和初始化关键的寄存器,然后再切换入保护模式。
● 保护模式:操作系统运行时最常用的模式。在该模式下,CPU的所有功能几乎都能得到使用,可以访问架构允许的所有物理地址空间。
● 虚拟 8086模式:该模式让 CPU在保护模式下为 8086程序虚拟实模式的运行环境,使这些程序在执行时无须从保护模式切换到实模式。
IA-32e 模式是x86在运行64位操作系统的时候使用的一种模式。带有64位扩展技术的处理器初始进入传统的页式地址保护模式,然后 PAE 模式被使能。IA-32e模式只能在装载64位操作系统的情况下进入,它包含两个子模式:64位模式和兼容模式。
1)64位模式用于运行在 64位操作系统上的 64位应用程序。它支持的特性如下:
● 64位线性地址结构。
● 现有的通用寄存器被加宽到64位(RAX、RBX、RCX、RDX、RSI、RDI、RBP、RSP)。
● 新增了8个通用寄存器(R8~R15)。
● 新增了8个128位流SIMD扩展寄存器(XMM8~XMM15)。
● 一个64位的指令指针(RIP)和一个新的RIP相关数据寻址模式。
● 对单一的代码、数据和栈空间能用平板地址空间。
● 扩展指令和新指令。
● 支持大于64GB的物理地址。
● 新的中断优先级控制机制。
2)兼容模式允许传统的 16位和 32位应用程序无须重新编译便运行在 64位操作系统下,然而虚拟8086模式、任务切换和栈参数复制特性在兼容模式下不被支持。
表1-1列出了目前x86-64所支持的模式及这些模式的特点。
表1-1 x86-64所支持模式的特点总结
根据表1-1,我们可以看出,在传统模式下,x86-64的CPU工作模式与传统的IA-32完全相同。在长模式下,x86-64对16位和32位代码进行了兼容(即兼容模式)。即使CPU上运行64位操作系统,传统的16位和32位代码也能在操作系统上运行。而且,由于x86-64兼容IA-32指令,这些代码在兼容模式下运行基本没有性能损耗。
1.2.5 x86-64的寄存器组
寄存器是软件操作CPU的最基本部件,x86架构的寄存器可以粗略地分为以下几类。
● 通用寄存器:用来保存程序运行时的临时变量栈指针等数据。
● 内存管理寄存器:包括段寄存器和描述符表寄存器。
● 标志寄存器:用来保存程序运行中的一些标志位信息,如溢出、开启中断与否、分支跳转等信息。
● 指令寄存器:用来保存指向当前指令的地址,又称PC指针。
● 控制寄存器:x86提供了5个控制寄存器—CR0~CR4,这些寄存器决定了CPU运行的模式和特征等。x86-64的64位模式引入了CR8,它被定义为任务优先级寄存器(TPR),操作系统能够基于中断的优先级别使用TPR来控制是否允许外部中断来中断处理器。
● 其他寄存器:包括调试寄存器(DR)、内存区域类型寄存器(MTRR)、机器检查寄存器以及性能监控寄存器等。
表 1-2 对运行在 64 位模式下的应用程序和运行在传统的 IA-32 环境下的应用程序中的一些寄存器数据结构进行对比分析。传统的IA-32环境包括那些存在于现有IA-32处理器中的传统模式、支持64位扩展技术的处理器中的兼容模式。
表1-2 x86-64的寄存器组
1.2.6 中断与异常
程序的执行往往不只是按顺序执行那么简单,一些异常和中断会打断顺序执行的程序流,转而进入一条完全不同的执行路径。本节将介绍现代CPU架构中的中断和异常机制,这对于读者理解处理器的工作很有帮助。
1. 中断架构
从某种意义上讲,现代计算机架构是由大量的中断事件驱动的。中断机制使外部硬件设备可以打断CPU当前的执行任务,使CPU为自己提供服务。
中断从设备经由“中断控制器”转发给CPU(MSI除外)。中断控制器发展至今,经历了PIC(Programmable Interrupt Controller,可编程中断控制器)和APIC(Advanced Programmable Interrupt Controller,高级可编程中断控制器)两个阶段。
PIC具有IR0~IR7共8个中断管脚可以与外部设备相连,其中IR0优先级最高,IR7优先级最低。PIC有如下三个重要的寄存器。
● IRR(Interrupt Request Register,中断请求寄存器):共8位,对应IR0~IR7这8个中断管脚。当某一位为1时,则代表该对应管脚收到中断,但还未提交给CPU。
● ISR(In Service Register,服务中寄存器):共 8 位。当某一位为 1时,则代表对应管脚的中断已经提交给CPU,但是CPU还没处理完。
● IMR(Interrupt Mask Register,中断屏蔽寄存器):共8位。当某一位置1时,则代表对应管脚的中断被屏蔽。
此外,PIC 还有一个 EOI 位,当 CPU 处理完一个中断时,通过写 EOI 位告知PIC中断处理完成。PIC向CPU提交中断的流程如下:
1)一个或多个 IR 管脚上产生电平信号,若对应的中断没有被屏蔽,IRR中相应位被置1。
2)PIC通过INT管脚通知CPU中断发生。
3)CPU通过INTA管脚应答PIC,表示中断请求收到。
4)PIC收到INTA应答之后,将IRR中具有最高优先级的管脚位清零,并设置ISR中的相应位。
5)CPU通过INTA管脚第二次发出脉冲,PIC收到脉冲后,将计算最高优先级中断对应的中断向量,并将它提交到数据线上。
6)等待CPU写EOI。收到EOI后,ISR中最高优先级的管脚位清零。
PIC只能在UP(单处理器)平台上工作,而无法用于MP(多处理器)平台。因此,APIC应运而生。APIC由两部分构成:位于CPU中的本地高级可编程中断控制器(Local Advanced Programmable Interrupt Controller,LAPIC)和位于主板南桥中 I/O 高级可编程中断控制器(I/O Advanced Programmable Interrupt Controller,IOAPIC)。LAPIC和IOAPIC的关系如图1-12所示。
图1-12 APIC系统架构
IOAPIC通常有24个不具有优先级的管脚,用于连接外部设备。当收到某个管脚的中断信号后,IOAPIC 根据操作系统设定的 PRT(Programmable Redirection Table)查找到管脚对应的RTE(Redirection Table Entry,PRT的表项)。通过RTE格式化出一条包含该中断所有信息的中断消息,再由系统总线交由特定CPU的LAPIC,LAPIC收到该消息后,择机将中断交给CPU处理。
LAPIC也有IRR、ISR和EOI寄存器,其中IRR和ISR为256位,EOI为32位,它们的功能与PIC中的相应设备基本相同。
在MP平台上,多个CPU要协同工作,于是APIC还可以提供处理器间中断(Inter-processor Interrupt,IPI),方便CPU之间相互通信。CPU通过LAPIC中的中断命令寄存器(Interrupt Command Register,ICR)向指定的一个或多个CPU发送中断。操作系统通过IPI来完成进程转移、中断平衡和TLB刷新等工作。
2. 异常架构
中断由外部设备产生,和CPU当前执行的指令无关。异常由CPU内部产生,其原因是CPU当前执行的指令出了问题。
异常根据产生的原因和严重性可以分为如下三类。
● 错误(Fault):由某种错误情况引起,一般可以被错误处理程序纠正。错误发生时,处理器将控制权交由对应的处理程序。前面所讲的缺页错误就属于此类。
● 陷阱(Trap):指在执行了一条特殊指令后引起的异常。陷阱是有意的异常,陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口(即系统调用)。Linux中用于实现系统调用的INT 80 指令就属于此类。
● 终止(Abort):指严重的不可恢复的错误,将导致程序终止。典型的是一些硬件错误。
3. 中断门与异常门
IDT 表是一个大数组,用于存放各种“门”,这些“门”是中断和异常通往各自处理函数的入口。当一个中断或异常发生时,CPU用它们对应的vector号索引IDT表以获得对应的“门”。IDT表的基地址存放在IDTR寄存器中,该寄存器与GDTR类似,由一个基地址字段(Base)和长度字段(Limit)构成。
中断门实际上是一种段描述符,称为系统描述符,由段描述符的S位控制。中断门的格式如图1-13所示。
图1-13 中断门的格式
其中,段选择符、偏移量字段可以看成一个逻辑地址,通过索引 GDT 将该逻辑地址转换成中断处理函数入口的线性地址。需要注意的是,中断门和陷阱门的 DPL 只在使用 INT n 指令引起中断或异常的时候才进行检查,硬件产生的中断或异常不检查。P字段代表该中断门是否有效,清零无效。
陷阱门和中断门类似,其格式如图1-14所示,各字段的含义与中断门相同。
图1-14 陷阱门的格式
中断门和陷阱门唯一的区别就是程序通过中断门跳转后,EFLAGS寄存器的IF位自动清零,中断关闭。而陷阱门没有这样的效果。
4. 中断与异常总结
最后,对中断与异常进行一个简要的总结,如表1-3所示。
表1-3 中断与异常
1.2.7 I/O架构
计算机所处理的任务其实只有两种:CPU运算和I/O操作。本节主要对x86的I/O架构进行介绍,为后面I/O虚拟化的内容做准备。
I/O(输入/输出)是CPU访问外部设备的方法。设备通常通过寄存器和设备RAM将自身功能展现给CPU,CPU通过读/写这些寄存器和RAM完成对设备的访问及其他操作。按访问方式的不同,x86架构的I/O分为如下两类。
1)端口I/O(后文简称为Port I/O):即通过I/O端口访问设备寄存器。x86有 65536个 8位的 I/O端口,编号为 0x0~0xFFFF。CPU将端口号作为设备端口的地址,进而对设备进行访问。这 65536个端口构成了 64KB的 I/O端口地址空间。I/O 端口地址空间是独立的,不是线性地址空间或物理地址空间的一部分。需要使用特定的操作命令IN/OUT对端口进行访问,此时CPU通过一个特殊的管脚标识这是一次I/O端口访问,于是芯片组知道地址线上的地址是I/O端口号并相应地完成操作。此外,2个或4个连续的8位I/O端口可以组成16位或32位的I/O端口。
2)内存映射I/O(Memory Map I/O,后文简称为MMIO):即通过内存访问的形式访问设备寄存器或设备RAM。MMIO要占用CPU的物理地址空间,它将设备寄存器或设备RAM映射到物理地址空间的某段地址,然后使用MOV等访存指令访问此段地址,即可访问到映射的设备。MMIO方式访问设备也需要进行线性地址到物理地址的转换,但是这个转换过程中的MMIO地址不可缓存到 TLB中。MMIO是一种更普遍、更先进的 I/O访问方式,很多 CPU架构都没有Port I/O,采用统一的MMIO方式。
1.2.8 DMA
直接内存访问(Direct Memory Access,后文简称为DMA)是所有现代计算机的重要特色。DMA允许设备绕开CPU直接向内存中复制或读取数据。如果设备向内存复制数据都经过CPU,则CPU会有大量中断负载,中断过程中, CPU对其他任务来讲无法使用,不利于系统性能的提高。通过DMA,CPU只负责初始化这个传输动作,而传输动作本身由 DMA 控制器(后文简称为DMAC)来实行和完成。在实现 DMA 传输时,由 DMAC 直接控制总线,在DMA 传输前,CPU 要把总线控制权交给 DMAC,结束 DMA 传输后,DMAC立即把总线控制权交回给CPU。
一个完整的DMA传输过程的基本流程如下。
1)DMA请求:CPU对DMAC进行初始化,并向I/O端口发出操作命令, I/O端口提出DMA请求。
2)DMA响应:DMAC对DMA请求进行优先级判别和屏蔽判别,然后向总线裁决逻辑提出总线请求。CPU执行完当前总线周期后释放总线控制权。此时,总线裁决逻辑发出总线应答,表示DMA已被响应,并通过DMAC通知I/O端口开始DMA传输。
3)DMA传输:DMAC获得总线控制权后,CPU即可挂起或只执行内部操作,由DMAC发出读/写命令,直接控制RAM与I/O端口进行DMA传输。
4)DMA结束:当完成规定的成批数据传送后,DMAC释放总线控制权,并向 I/O 端口发出结束信号。当 I/O 端口接收到结束信号后,停止 I/O 设备的工作并向 CPU提出中断请求,使 CPU 执行一段检查本次 DMA传输操作正确性判断的代码,并从不介入的状态退出。
由此可见,DMA无须CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件(DMAC)为 RAM 与 I/O 设备开辟了一条直接传送数据的通路,极大地提高了CPU效率。需要注意的是,DMA操作访问的必须是连续的物理内存。DMA传输的过程如图1-15所示。
图1-15 DMA传输示意图
1.2.9 时钟
在现代计算机架构中,时钟的地位非常重要,操作系统中的很多事件都是由时钟驱动的,如进程调度、定时器等。
根据时钟工作方式的不同,可以分成如下两类。
● 周期性时钟(Periodic Timer):这是最常见的方式,时钟以固定频率产生时钟中断。周期性时钟通常会有一个计数器,要么以固定值递减到0产生中断,如PIT(Programmable Interrupt Timer,可编程中断时钟);要么固定增长,当达到某个阈值时产生中断,同时自动将阈值增加一个固定值,计数器继续递增,如HPET(High Precision Event Timer,高精度时间时钟)。
● 单次计时时钟(One-shot Timer):大多数时钟都可以配置成这种方式,例如PIT、HPET。其工作方式和到达阈值产生中断的周期性时钟类似,只是产生中断后阈值不会自动增加,而是需要时钟中断处理函数等软件来增加该阈值。这为软件提供了动态调整下一次时钟中断到来时间的能力。
x86平台提供了多种时钟,包括PIT、RTC(Real Time Clock,实时时钟)、TSC(Time Stamp Counter,时间戳计数器)、LAPIC Timer和HPET等。操作系统可以根据需要使用其中的一种或多种时钟,但是同时使用多个时钟将带来过多的时钟中断,从而影响系统的性能。在有高精度时钟可用的时候,现代操作系统往往禁用低精度的时钟,并根据需要使用高精度的时钟模拟低精度的时钟。