第3章 保护模式内存管理
本章描述Intel 64和IA-32架构的保护模式内存管理设施,包括物理内存需求、分段机制和分页机制。
请见:第5章《保护》(对处理器的保护机制的描述)和第20章《8086仿真》(对实模式和虚拟8086模式下的内存寻址保护的描述)。
3.1 内存管理概述
IA-32架构的内存管理设施可以被分为两部分:分段和分页。分段提供了隔离每个代码、数据和堆栈模块的机制,从而使多个程序(或任务)可以运行在同一个处理器上而不会互相干扰。分页机制可以实现传统的需要分页的虚拟内存系统,在这样的系统上程序执行环境的不同段会根据需要映射到物理内存。分也还可以用来提供多个任务之间的隔离。在保护模式中,必须使用一些分段的形式。没有标志位可以用来禁用分段机制,但分页机制的使用是可选的。
这两个机制(分段和分页)可以被设置来支持简单的单程序(或单任务)系统、多任务系统或使用共享内存的多处理器系统。
正如图3-1显示的,分页提供了将处理器可寻址的内存空间(称作“线性地址空间”)划分为更小的受保护的地址空间(称作“段”)的机制。段可以用来保存程序的代码、数据和堆栈,或者系统的数据结构(例如TSS或LDT)。如果多于一个程序(或任务)运行在一个处理器上,每个程序可以被分配到它自己的段集。然后处理器会强制保证这些段之间的边界和保证一个程序不会写到另一程序的段而干扰其运行。分段机制还允许对段进行分类从而限制对某些种类的段的操作。
系统中的所有段都被包含在处理器的线性地址空间中。在一个特定的段中定位一个字节,需要提供其逻辑地址(又称“远指针”)。一个逻辑地址包含一个段选择子和一个偏移量。段选择子是一个段的唯一标识符,它提供该段的段描述符在描述符表(如全局描述符表GDT)中的偏移量。每一个段都有一个指定其大小、访问权限、特权级、段类型和首个字节的线性地址(段的基础地址)的段描述符。逻辑地址的偏移量部分会被加到段的基础地址来定位段中的一个字节。因此基础地址加上偏移量就构成了处理器的线性地址空间中的一个线性地址。
如果不使用分页,处理器的线性地址空间会直接映射到处理器的物理地址空间。物理地址空间的定义是处理器能在其寻址总线上产生的那个地址范围。
因为多任务计算机系统通常定义比经济上可包含的物理内存大得多的线性地址空间,一些虚拟化线性地址空间的方法是需要的。线性地址空间的虚拟化是通过处理器的分页机制实现的。
|
图3-1、分段和分页
分页支持一个“虚拟内存”环境。在“虚拟内存”环境中,一个大的线性地址空间可以用不多的物理内存(RAM和ROM)和一些磁盘存储来仿真。使用分页时,每个段会被分成若干页(通常每页4 KBytes)。一帧页或存在物理内存中或存在磁盘上。操作系统或管理层程序维护一个页目录和一系列页表来记录每一页的信息。当一个程序(或任务)试图访问线性地址空间中的一个地址,处理器会使用页目录和页表来把线性地址翻译成物理地址,然后再执行请求的读写内存操作。
如果被访问的页当前不在物理内存中,处理器会中断程序的执行(通过产生一个页面错误异常)。接下来操作系统或管理层程序要将相应页面从磁盘中读到物理内存中,再返回继续执行原程序。
若操作系统或管理层程序正确地实现分页,物理内存和磁盘之间的页面交换对于程序的正确运行是透明的。即使为16位的IA-32处理器写的程序在虚拟8086模式中,也可以(透明地)被分页。
3.2 使用段
被IA-32架构支持的分段机制可以被用来实现许多不同的系统设计,从使用最少分段的平坦模型,到建立健壮操作环境的多段模型。
下面会给出使用分段来提供内存管理性能与可靠性的几个例子。
3.2.1 基本平坦模型
最简单的内存模型是基本“平坦模型”。在此模型中,操作系统和应用程序可以访问一个连续的、不分段的地址空间。有着最大的可拓展性,基本平坦模型对系统设计者和应用程序编程者隐藏了分段机制。
要在IA-32架构实现基本平坦内存模型,至少要创建两个段描述符,一个用来引用代码段,一个用来引用数据段(见图3-2)。但这两个段都映射到全部的线性地址空间,即两个段描述符都有相同的基础地址0和相同的段界限4 GBytes。通过把段界限设为4 GBytes,分段机制可以防止产生内存引用越界异常,即使在那特定地址没有物理内存。ROM(EPROM)一般位于物理地址空间的顶部,因为处理器从FFFF_FFF0h开始执行。RAM(DRAM)位于地址空间的底部,因为DS数据段寄存器在复位后初始化为0,初始的地址空间为0。
|
图3-2、平坦模型
3.2.2 受保护的平坦模型
受保护的平坦模型与基本平坦模型类似,除了段界限被设为物理内存真实存在的地址范围(见图3-3)。试图访问不存在的内存会产生一个一般保护异常(#GP)。这个模型提供了最少层次的应对某些程序漏洞的硬件保护。
要提供更多的保护,可以向这个受保护的平坦模型加入更多复杂性。例如,要有提供用户和管理者的代码和数据的隔离的分页机制,需要定义四个段:用户层的特权级3的代码段和数据段,和管理层的特权级0的代码段和数据段。通常这些段都是相互重叠的,从线性地址空间的地址0开始。这个有一个简单分页结构的平坦分段模型可以保护操作系统免受应用程序的干扰;通过为每个任务或进程建立一个独立的分页结构,它还可以保护每个应用程序不受其他程序的干扰。几个流行的多任务操作系统都使用这种简单的设计。
|
图3-3、受保护的平坦模型
3.2.3 多段模型
一个多段模型(例如图3-4表示的一个)使用分段机制的完整功能来提供对代码、数据结构和程序与任务的硬件强制保护。在此,每个程序(或任务)都有它自己的段描述符表和自己的段。这些段可以为相应程序完全私有,也可以被多个程序共享。对所有段和对每个运行的程序的执行环境的访问都由硬件控制。
访问检查不仅可以用来防止超过段界限的地址引用,还可以用来防止对特定的段的某些操作。例如,代码段应是只读的段,硬件可以用来防止对代码段的写操作。段的访问权限信息还可以用来对不同特权级提供保护。保护层级可以用来保护操作系统的过程免受应用程序的未授权访问。
|
图3-4、多段模型
3.2.4 IA-32e模式中的分段
在Intel 64架构的IA-32e模式中,分段的效果取决于处理器是运行在兼容模式还是64位模式。在兼容模式中,分段的功能与传统的16位或32位的保护模式相同。
在64位模式中,分段一般(但不完全)是关闭的,从而得到一个平坦的64位的线性地址空间。处理器将CS、DS、ES、SS的段基础地址都看作0,使线性地址等效于有效地址。FS和GS段例外,这两个(存放有段基础地址的)段寄存器可以用作计算线性地址的额外基础地址寄存器。它们帮助对本地数据和特定操作系统数据结构的寻址。
要注意的是处理器在64位模式中不进行段界限检查。
3.2.5 分页和分段
分页可以用在任何一种图3-2、3-3和3-4描述的分段模型中。处理器的分页机制将(段映射的)线性地址空间划分为若干页(如图3-1所示)。这些线性地址空间的页又被映射到物理地址空间的页。分页机制提供了一些页面层次的保护设施,可以与或不与分段的保护设施并用。例如,它让读写保护强制在页面层次上。分页机制还提供了在页面层次上的两层的用户-管理者保护机制。
3.3 物理地址空间
在保护模式中,IA-32架构提供了一个常规的4 GBytes(232字节)地址空间。这是处理器能在其地址总线上寻址的地址空间。这个地址空间是平坦的(未分段的),地址范围从0到0FFFF FFFFh。这个物理地址空间可以被映射到可读写内存、只读内存和内存映射I/O。本章描述的内存映射设施可以用来把这个物理内存分成若干段和/或若干页。
从Pentium Pro处理器开始,IA-32架构还支持到64 GBytes(236字节)的物理地址空间的拓展,即最大的物理地址是0F FFFF FFFFh。这个拓展可以用以下任一方式开启:
使用物理地址拓展(PAE)标志位,即控制寄存器CR4的bit 5。
使用36位页面大小拓展(PSE-36)特性(从Pentium III处理器引入)。
因此可支持36位的物理地址。更多关于36位物理地址的信息请见第4章《分页》。
3.3.1 Intel® 64处理器和物理地址空间
支持Intel 64架构(CPUID.8000 0001h:EDX[29] = 1)的处理器上,物理地址范围的大小是与实现相关的,并由CPUID.8000 0008h:EAX[bits 7 – 1] 指定。
关于此功能在EAX中的返回值的格式,请见的《Intel®64 和 IA-32 架构的软件开发者手册》卷2A的第3章《CPUID——CPU识别》。请参考第4章《分页》。
3.4 逻辑和线性地址
在保护模式的系统架构层次中,处理器使用两个阶段的地址翻译来获得物理地址:逻辑地址翻译和线性地址空间分页。
即使用最少的分段,处理器的地址空间中的每个字节都要用逻辑地址访问。一个逻辑地址由一个16位的段选择子和一个32位的偏移量构成(见图3-5)。段选择子识别该字节所在的段,而偏移量指定该字节在段中相对于段的基础地址的位置。
处理器将每个逻辑地址翻译成线性地址。一个线性地址是处理器的线性地址空间中的一个32位的地址。像物理地址空间一样,线性地址空间是一个平坦的(未分段的)232字节的地址空间,其地址范围从0到0FFFF FFFFh。线性地址空间包含所有定义了的段和系统表。
要把一个逻辑地址翻译成一个线性地址,处理器要做如下工作:
1、 使用段选择子中的偏移量来定位GDT或LDT中该段的段选择子,并读取它到处理器中。(这一步只在一个新的段选择子被加载到一个段寄存器中时是需要的)。
2、 检查段描述符,检查其访问权限和范围来确保该段是可访问的并且偏移量没有超过段界限。
3、 把偏移量加到从段描述符中读取的段的基础地址中,得到一个线性地址。
|
图3-5、逻辑地址到线性地址的翻译
如果不使用分页,处理器直接将线性地址映射为物理地址(即线性地址会被输出到处理器的地址总线)。如果线性寻址空间被分页,则要使用第二层次的地址翻译来将线性地址翻译成物理地址。
请见第4章《分页》。
3.4.1 IA-32e模式中的逻辑地址翻译
在IA-32e模式中,Intel 64处理器使用上述的步骤来将一个逻辑地址翻译成一个线性地址。在64位模式中,偏移量和段的基础地址都是64位而不是32位的。线性地址也是64位宽的,而且服从规范格式的要求。
每个代码段描述符提供一个L位。这个位指示该代码段是64位代码还是传统的32位代码。
3.4.2 段选择子
一个段选择子是一个段的一个16位的的标识符(见图3-6)。它不直接指向该段,而是指向定义该段的段描述符。一个段描述符包含以下项目:
位15到3——选择GDT或LDT中的最多8192个描述符中的一个。处理器把索引值乘以8(一个段描述符的字节数)然后加上GDT或IDT的基础地址(分别从GDTR或LDTR寄存器得到)。 | |
TI(表类型指示)标志位 | |
| 位2——指定使用的描述符表:清0则选择GDT,设1则选择当前的LDT。 |
请求的特权级(RPL) | |
| 位1和0——指定选择子的特权级。特权级的范围是从0到3,0是最优特权级而3是最次特权级。对RPL、程序(或任务)的CPL和段描述符特权级(DPL)的关系的描述,请见5.5《特权级》。 |
|
图3-6、段选择子
处理器不使用GDT的第一项。一个指向该项的段选择子(索引值为0且TI位为0的段选择子)被用作一个“空段选择子”。若(除CS或SS寄存器外的)段寄存器被加载一个空选择子,处理器不会产生异常,但当用存放空选择子的单寄存器来访问内存时会产生异常。一个空选择自可以用来初始化不被使用的段寄存器。加载空的段选择子到CS或SS寄存器会导致一个一般保护异常(#GP)产生。
段选择子作为指针变量的一部分,对应用程序是可见的,但选择子的值通常是由链接器而非应用程序指定或修改的。
3.4.3 段寄存器
为了减少地址翻译的时间和编码的复杂性,处理器提供了可存放最多6个段选择子的寄存器(见图3-7)。每个段寄存器支持一个特定种类的内存引用(代码、堆栈或数据)。为了实际中所有种类的程序的运行,至少代码段(CS)、数据段(DS)和堆栈段(SS)寄存器必须被加载合法的段选择子。处理器还提供了三个额外的数据段寄存器(ES、FS和GS),它们能让当前执行的程序(或任务)可以使用额外的数据段。
一个程序要访问一个段,必须先加载该段的段选择子到一个段寄存器中。尽管一个系统可以定义上千个段,只有6个是可以即时使用的。对于其他的段,可以通过在程序运行时加载其段选择子到段寄存器中来使其可用。
|
图3-7、段寄存器
每个段寄存器都有一个“可见”部分和一个“不可见”部分。(不可见部分有时被称作“描述符缓存”或“影子寄存器”。)当一个段选择子被加载到一个段寄存器的可见部分时,处理器还会将段选择子指向的段描述符中的基础地址、段界限和访问控制信息加载到段寄存器的不可见部分。缓存在段寄存器中的信息(可见的和不可见的)让处理器翻译地址时,不必花费额外的总线周期来从段描述符读取基础地址、段界限等。若系统中有多个处理器都有对同一个描述符表的访问权,则软件有责任在修改描述符表时重新加载段寄存器。否则,在内存中的描述符被修改后,缓存在段寄存器的旧的段描述符还会被继续使用。
两种加载指令可以加载段寄存器:
1、 直接加载指令,例如MOV、POP、LDS、LES、LSS、LGS和LFS指令。这些指令显式地引用段寄存器。
2、 隐含的加载指令,例如CALL、JMP和RET指令的远指针版本,SYSENTER和SYSEXIT指令,还有IRET、INTn、INTO和INT3指令。这些指令改变CS寄存器的内容(有时还有其他段寄存器)作为其伴随操作。
MOV指令还可以用来保存段寄存器的可见部分到通用寄存器中。
3.4.4 IA-32e模式中的段加载指令
因为ES、DS和SS段寄存器在64位模式中不被使用,段描述符寄存器中它们的位域(基础地址、段界限和属性)会被忽略。一些段加载指令会变为非法(例如LDS、POP ES)。引用ES、DS或SS段的地址,计算时会把段的基础地址看作0。
处理器检查所有线性地址引用是否符合规范格式而不会进行段越界检查。模式切换不会改变段寄存器或对应的描述符寄存器的内容。除非执行显式的段加载,在64位模式中这些寄存器不会被更改。
为了进入兼容模式和运行兼容模式的程序,段加载指令(MOV到段寄存器、POP段寄存器)在64位模式中正常工作。系统描述符表(GDT或LDT)中的一项会被读取加载到段描述符寄存器(不可见部分)中。描述符寄存器的基础地址、段界限和属性位域都会被加载。但是数据和堆栈段选择子和描述符寄存器的内容会被忽略。
若在64位模式中使用FS或GS段超越前缀,它们的基础地址会在线性地址计算中被使用:(FS或GS)的基础地址 + 索引 + 位移。接着FS的基础地址和GS的基础地址会被自动拓展到完整的线性地址的大小。计算得出的有效地址是有符号的(可正可负);计算得出的线性地址必须符合规范。
在64位模式中,用FS或GS段超越前缀来访问内存不会有对段界限和段属性的检查。常规的向FS和GS的段加载(MOV到FS或GS和POP FS或GS)会加载一个标准的32位基础地址到段描述符寄存器(不可见部分)中。基础地址高于标准的32位的部分会被清0以兼容少于64位的实现。
FS和GS的隐藏的描述符寄存器的基础地址位域物理地映射到MSR,来允许加载64位实现支持的所有地址位。CPL = 0的软件可以使用WRMSR指令来加载所有支持的线性地址位到FS或GS的基础地址。写到64位的FS和GS的基础地址寄存器的地址必须符合规范。试图将不规范的地址写到这两个寄存器的WRMSR指令会触发#GP错误。
在兼容模式中,FS和GS段超越前缀的操作行为跟32位模式中的一样,无论隐藏的描述符寄存器的基础地址位域中的地址的高32位是什么。兼容模式计算有效地址时忽略高32位。
一个新的64位模式的指令SWAPGS可以用来加载GS的基础地址。SWAPGS会交换IA32_KernelGSbase MSR中的内核数据结构指针和GS的基础地址寄存器的值。然后内核可以使用有GS前缀的常规内存引用来访问内核数据结构。试图写一个不规范的值(用WRMSR)到IA32_KernelGSbase MSR中会触发#GP错误。
3.4.5 段描述符
一个段描述符是GDT或LDT中给处理器提供段的大小、位置、访问控制和状态信息的一个数据结构。段描述符一般由编译器、连接器、加载器,或者操作系统或管理层程序创建,而非应用程序创建。图3-8说明了所有类型的段描述符的一般格式。
|
图3-8、段描述符
段描述符中的这些标志位和位域的说明如下:
段界限位域 | 指定段的大小。处理器将两个段界限位域合并到一起来产生一个20位的值。基于G(粒度)标志位的设置,处理器有两种方式翻译段界限: l 如果粒度标志位为0,段的大小的范围从1字节到1 MByte,增长单位是1字节。 l 如果粒度标志位为1,段的大小的范围从4 KBytes到4 GBytes,增长单位是4 KBytes。 基于该段是向上延伸还是向下延伸,处理器使用两种不同的方法对待段界限。关于段的类型的更多信息,请见3.4.5.1《代码段和数据段描述符类型》。对于向上延伸的段,逻辑地址的偏移量的范围是0到段界限。大于段界限的偏移量会引起一般保护异常(#GP,除堆栈段SS以外的所有段)或者堆栈错误异常(#SS,针对堆栈段SS)。对于向下延伸的段,段界限的功能相反;根据B标志位的不同设置,偏移量的范围从(段界限+1)到0FFFF FFFFh或0FFFFh。小于等于段界限的偏移量会引起一般保护异常或者堆栈错误异常。减小向下延伸的段的段界限的值会在段的地址空间的底部而非顶部分配新内存。IA-32架构的堆栈总是向下延伸,使这个机制可以方便地拓展堆栈。 |
基础地址位域 | |
| 定义该段的字节0在4-GByte的线性地址空间中的位置。处理器将三个基础地址位域合并在一起来产生一个单一的32位地址。段的基础地址应该被对齐到16字节的边界,尽管16字节的对齐不是必需的,这样对其允许程序通过将代码和数据对齐到16字节的边界来获得最优性能。 |
类型位域 | 指定该段或门的类型并指定对该段的访问类型和该段的增长方向。对此位域的解释取决于描述符类型标志位指定为应用程序(代码或数据)描述符还是系统描述符。代码段、数据段和系统描述符对应的类型位域的编码是不同的(见图5-1)。关于此位域如何用来指定代码段和数据段等类型,请见3.4.5.1《代码段和数据段描述符类型》。 |
S(描述符类型)标志位 | |
| 指定此段描述符是对应一个系统段(清0)还是一个代码或数据段(设1)。 |
DPL(段特权级,Descriptor Privilege Level)位域 | |
| 指定该段的特权级。特权级的范围从0到3,0是最优特权级,3是最次特权级。DPL用来控制对该段的访问。对RPL、程序(或任务)的CPL和段描述符特权级(DPL)的关系的描述,请见5.5《特权级》。 |
P(段存在,Segment-Present)标志位 | |
| 指示该段是否存在内存里,设1则存在,清0则不存在。若此位为0,指向此描述符的段选择子被加载到段寄存器时,处理器会产生一个段不存在异常(#NP)。内存管理软件可以用此位来控制在给定时间一个段是否被加载到物理内存中。它提供了除了分页以外的管理虚拟内存的控制能力。图3-9显示了段存在标志位为0时段描述符的格式。当此位为0,操作系统或管理层程序可以自由使用标记为“可用”的区域来储存自己的数据,例如缺失段的位置。 |
图3-9、段存在标志位为0的段描述符 | |
D/B(默认操作数/堆栈指针)大小 和/或 上下界标志位 | |
| 根据段描述符对应的是可执行的代码段、向下延伸的数据段还是堆栈段,有着不同的功能。(对于32位的代码段和数据段,此标志位必须为1;对于16位的代码段和数据段,此标志位必须为0。) l 对于可执行代码段:此位叫做D标志位,它指示有效地址和段中指令引用的操作数的默认长度。若此位为1,则是32位的地址和32位或8位的操作数;若此位为0,则是16位的地址和16位或8位的操作数。指令前缀66h可以用来指定非默认的操作数长度,前缀67h可以用来指定非默认的地址长度。 l 对于堆栈段(由SS寄存器指向的数据段):此位叫做B(大)标志位,它指定非显式的堆栈操作(例如push、pop和call)的堆栈指针的大小。若此位为1,则使用存放在32位的ESP寄存器中的32位堆栈指针;若此位为0,则使用存放在16位的SP寄存器中的16位堆栈指针。如果该堆栈段被设为向下延伸的数据段(下一段将描述),B标志位还指定对斩断的上界。 l 对于向下延伸的数据段:此标志位叫做B标志位,它指定段的上界。若此位为1,则上界为0FFFF FFFFh(4 GBytes);若此位为0,则上界为0FFFFh(64 KBytes)。 |
G(粒度,Granularity)标志位 | |
| 决定段界限位域的尺度。若粒度标志位为0,则段界限被认为是以1字节为单位;若粒度标志位为1,则段界限被认为是以4-KByte为单位。(此标志位不影响基础地址的粒度,基础地址总是以1字节为粒度。)若粒度标志位为1,检查偏移量是否越界时,不会检查偏移量的最低12位。例如,若粒度标志位为1,为0的段界限也会导致从0到4095的偏移量合法。 |
L(64位代码段)标志位 | |
| 在IA-32e模式中,段描述符的第二个双字的位21指示一个代码段包含的是64位的代码还是32位或16位的代码。1表示代码段应执行在64位模式,0表示代码段应执行在兼容模式。如果L标志位为1,D标志位必须为0。若不是在IA-32e模式或对于非代码段,位21是保留的并应设为0。 |
AVL可用位 | 段描述符的第二个双字的位20提供给系统软件自行按需使用。 |
3.4.5.1 代码段和数据段段描述符类型
当段描述符中的S(描述符类型)标志位为1时,该描述符对应一个代码段或者一个数据段。接下来,类型位域中的最高位(段描述符第二个双字的11位)决定描述符对应一个代码段(清0)还是一个数据段(设1)。
对于数据段,类型位域的低3位(位8、9、10)分别指示已访问(A)、可写(W)和拓展方向(E)。关于代码段和数据段的描述符的类型位域的编码,请见表格3-1。根据可写位的不同设置,数据段可以是只读或者可读写的段。
堆栈段是必须为可读写的数据段。把不可写的数据段的段选择子加载到SS寄存器会触发一般保护异常(#GP)。如果堆栈段的大小需要动态的改变,堆栈段可以是一个向下延伸的数据段(拓展方向位设1)。在此,动态增加堆栈段的界限会使堆栈空间增加到堆栈的底部。如果一个堆栈段要设计为大小是静态的,则堆栈段可以是向上延伸的也可以是向下延伸的。
已访问位指示该段在上次操作系统或管理层程序将此位清0之后,是否又被访问。每当该段的段选择子被加载到一个段寄存器,处理器就自动把此位设1,只要包含该段描述符的内存的类型支持处理器的写操作。此位在显式的清0之前会保持为1。此位可以用于管理虚拟内存或者调试。
表格3-1、代码段和数据段的类型
类型位域 | 描述符类型 | 说明 | ||||
十进制 | 11 | 10 | 9 | 8 | ||
0 | 0 | 0 | 0 | 0 | 数据 | 只读 |
1 | 0 | 0 | 0 | 1 | 数据 | 只读、已被访问 |
2 | 0 | 0 | 1 | 0 | 数据 | 可读写 |
3 | 0 | 0 | 1 | 1 | 数据 | 可读写、已被访问 |
4 | 0 | 1 | 0 | 0 | 数据 | 只读、向下延伸 |
5 | 0 | 1 | 0 | 1 | 数据 | 只读、向下延伸、已被访问 |
6 | 0 | 1 | 1 | 0 | 数据 | 可读写、向下延伸 |
7 | 0 | 1 | 1 | 1 | 数据 | 可读写、向下延伸、已被访问 |
|
| C | R | A |
|
|
8 | 1 | 0 | 0 | 0 | 代码 | 只可执行 |
9 | 1 | 0 | 0 | 1 | 代码 | 只可执行、已被访问 |
10 | 1 | 0 | 1 | 0 | 代码 | 可读可执行 |
11 | 1 | 0 | 1 | 1 | 代码 | 可读可执行、已被访问 |
12 | 1 | 1 | 0 | 0 | 代码 | 只可执行、一致的 |
13 | 1 | 1 | 0 | 1 | 代码 | 只可执行、一致的、已被访问 |
14 | 1 | 1 | 1 | 0 | 代码 | 可读可执行、一致的 |
15 | 1 | 1 | 1 | 1 | 代码 | 可读可执行、一致的、已被访问 |
对于代码段,类型位域的低3位分别指示已访问(A)、可读(R)和一致的(C)。根据可读标志位的不同设置,代码段可以是只可执行的或者可读可执行的。可读可执行段可能用在常量或其他静态数据跟指令代码放在一起的ROM上。在此,读取代码段的数据可以用CS超越前缀的指令,或者将代码段的段选择子加载到数据段寄存器(DS、ES、FS或GS)中。在保护模式中,代码段是不可写的。
代码段可以是一致的或者非一致的。跳转执行更优特权级的一致代码段中的代码,特权级与跳转前的特权级保持一致。试图跳转执行其它特权级的非一致代码段中的代码会引起一般保护异常(#GP)的产生,除非是使用一个调用门或任务门(关于一致和非一致代码段的更多信息,请见5.8.1《直接调用或跳转到代码段》)。不用访问受保护的设施的系统实用程序和一些异常(例如除以零或者溢出)的处理程序可以加载到一致代码段。要保护免受更次特权级的程序和过程干扰的系统实用程序应该放在非一致代码段。
注意
程序不能用调用或跳转指令转移到更次特权级(数值上更大的特权级)的代码段,无论目标代码段是一致的还是非一致的。试图这样转移的指令会引起一个一般保护异常。
所有数据段都是非一致的,这意味着它们不能被更次特权级的程序或过程(执行在数值上更大的特权级的代码)访问。但是不像代码段,数据段可以被更优特权级的程序或过程(执行在数值上更小的特权级的代码)访问而不需要使用特殊的访问门。
如果GDT或LDT中的该段描述符被放在ROM中,若软件或处理器试图更改(写入)在ROM中的段描述符,处理器会进入一个无限循环。为了避免这个问题,请将放在ROM中的所有段描述符的已访问位设1。另外,不要编写试图修改在ROM中的段描述符的代码。
3.5 系统描述符类型
若段描述符的S(描述符类型)标志位为0,则描述符的类型是系统描述符。处理器能识别以下类型的系统描述符:
l 局部描述符表(Local Descriptor-Table,LDT)段描述符。
l 人物状态段(Task-State Segment,TSS)描述符。
l 调用门(Call-Gate)描述符。
l 中断门(Interrupt-Gate)描述符。
l 陷阱门(Trap-Gate)描述符。
l 任务门(Task-Gate)描述符。
这些描述符的类型可分成两类:系统段描述符和门描述符。系统段描述符指向系统段(LDT和TSS段)。门描述符本身就是“门”,它们存放指向代码段中的过程的入口点的指针(调用、中断和陷阱门)或者存放TSS的段选择子(任务门)。
表格3-2说明了系统段描述符和门描述符的类型位域的编码。要注意的是IA-32e模式中的系统描述符是16字节而非8字节的。
关于系统段描述符和门描述符的更多信息,请见:3.5.1《段描述符表》,5.8.3《调用门》,6.11《IDT描述符》,7.2.2《TSS描述符》,7.2.5《任务门描述符》。
表格3-2、系统段和门描述符的类型
类型位域 | 说明 | |||||
十进制 | 11 | 10 | 9 | 8 | 32位模式 | IA-32e模式 |
0 | 0 | 0 | 0 | 0 | 保留 | 一个16位的描述符的高8字节 |
1 | 0 | 0 | 0 | 1 | 16位TSS(可用) | 保留 |
2 | 0 | 0 | 1 | 0 | LDT | LDT |
3 | 0 | 0 | 1 | 1 | 16位TSS(忙) | 保留 |
4 | 0 | 1 | 0 | 0 | 16位调用门 | 保留 |
5 | 0 | 1 | 0 | 1 | 任务门 | 保留 |
6 | 0 | 1 | 1 | 0 | 16位中断门 | 保留 |
7 | 0 | 1 | 1 | 1 | 16位陷阱门 | 保留 |
8 | 1 | 0 | 0 | 0 | 保留 | 保留 |
9 | 1 | 0 | 0 | 1 | 32位TSS(可用) | 64位TSS(可用) |
10 | 1 | 0 | 1 | 0 | 保留 | 保留 |
11 | 1 | 0 | 1 | 1 | 32位TSS(忙) | 64位TSS(忙) |
12 | 1 | 1 | 0 | 0 | 32位调用门 | 64位调用门 |
13 | 1 | 1 | 0 | 1 | 保留 | 保留 |
14 | 1 | 1 | 1 | 0 | 32位中断门 | 64位中断门 |
15 | 1 | 1 | 1 | 1 | 32位陷阱门 | 64位陷阱门 |
3.5.1 段描述符表
一个段描述符表是存放段描述符的一个数组(见图3-10)。一个描述符表的长度是可改变的。一个描述符表最多可以包含8192(213)个8字节的描述符。描述符表可分为两种:
l 全局描述符表(Global Descriptor Table,GDT)
l 局部描述符表(Local Descriptor Table,LDT)
每个系统必须有且只有一个定义的GDT,它被系统上的所有程序和任务使用。可以有选择地定义一个或多个LDT。例如,可以为每个运行的独立的任务分别定义一个LDT,也可以让一些或全部任务共享一个LDT。
GDT本身不是一个段,它是线性地址空间中的一个数据结构。GDT的基础线性地址和界限必须被加载到GDTR寄存器(见2.4《内存管理寄存器》)。GDT的基础地址应该被对齐到一个8字节的边界来获得最佳的处理器性能。GDT的界限的值以1字节为单位。跟段的越界检查一样,GDT的界限值加上基础地址的和就是最后一个合法的字节的地址,因此界限值为0的结果是恰好一个合法的字节。因为段描述符总是8个字节长,GDT的界限总应该是8的整数倍减1(8N – 1)。
|
图3-10、全局描述符表和局部描述符表
处理器不会使用GDT的第一个描述符。把这个“空描述符”的段选择子加载到一个数据段寄存器(DS、ES、FS或GS)不会触发异常,但试图用这个描述符访问内存一定会触发一般保护异常(#GP)。因为数据段寄存器在复位时会被初始化为这个段选择子,意外地用未使用的段寄存器来引用内存会引发异常。
LDT位于LDT类型的系统段中。GDT必须包含一个LDT段的段描述符。如果系统支持多个LDT,每个LDT必须在GDT中有其独立的段选择子和段描述符。一个LDT的段描述符可以放在GDT的任何位置。关于LDT段描述符类型的更多信息,请见3.5《系统描述符类型》。
要访问一个LDT,可以用其段描述符。为了减免地址的翻译,在访问LDT时,该LDT的段选择子、基础线性地址、界限和访问权限都被保存在LDTR寄存器中(请见2.4《内存管理寄存器》)。
当GDTR寄存器被保存(使用SGDT指令)时,一个48位的“伪描述符”会被存到内存中(见图3-11的上半部分)。为了避免用户模式(特权级3)中的对齐检查错误,伪描述符应被放在一个奇的字地址(地址除以4的余数为2)。这会使处理器保存一个对齐的字,接着一个对齐的双字。用户模式的程序一般不会保存伪描述符,但这种对齐伪描述符的方法可以避免产生对齐检查错误的可能性。同样的对齐方法也应该用在使用SIDT指令来保存IDTR寄存器的时候。而保存LDTR或任务寄存器(分别使用SLDT或STR指令)时,伪描述符应被放在一个双字地址(地址可被4整除)。
|
图3-11、伪描述符格式
3.5.2 IA-32e模式中的段描述符表
在IA-32e模式中,一个段描述符表最多可以包含8192(213)个8字节的描述符。段描述符表中的一个项可以是8字节的。系统描述符被拓展到16字节(占用两个项的空间)。
GDTR和LDTR寄存器被拓展到存放64位的基础地址。对应的伪描述符是80位的。(请见图3-11的下半部分)。
以下的系统描述符被拓展到16字节:
l 调用门描述符(请见5.8.3.1《IA-32e模式调用门》)
l IDT门描述符(请见6.14.1《64位模式IDT》)
l LDT和TSS描述符(请见7.2.3《64位模式中的TSS描述符》)