第三章 保护模式的内存管理【1】
【作者:lion3875 原创文章 参考文献《Intel 64 and IA-32 system programming guide》】
IA-32保护模式内存管理功能分为三部分,即物理内存请求、段机制以及分页机制,下面将一一道来。
3.1 内存管理概述
IA-32内存管理功能,被归结为两大部分,即分段与分页。分段提供了一种隔离代码段、数据段、堆栈段的机制,以便于多道程序在同一处理器上运行的时候,不会相互影响。分页机制实现了虚拟内存系统的常规页需求,就是在必要时,可以将一个程序运行环境的某一部分映射到物理内存当中,不需要时,也可以从物理内存中换出,分页还可以用于隔离多任务。当处于保护模式中时,段机制是默认被启用的,请记住,段机制是无法被关闭掉的,然而分页机制却是可选的。
这两种机制(分段、分页),都可以用来支持单任务系统、多任务系统或多处理器系统的内存共享。
如图3-1所示,分段机制将处理器的大块可寻址内存空间(线性地址空间),分割成小块的地址空间,那就是段,段在被访问时是受严格保护的。段可以用于保存程序的代码、数据、堆栈资源,还可以保存系统级数据结构(如TSS、LDT)。如果多个程序(或任务)同时运行在同一处理器上,那么每个程序都必须要有自己的段。处理器会强行在每个程序的段之间划清界线,以确保一个程序不会将数据写入另一个正在运行的程序的段,从而造成不期望出现的结果。段机制还允许将段进行分类,从而实现对特定类型的段操作的限制。
系统中所有的段都被包含在处理器的线性地址当中。要定位到一个特定段内的某个字节,需要使用逻辑地址。逻辑地址包含了段选择子与段内偏移量,段选择子是一个索引,用来在描述符表中(例如GDT)定位某个段的段描述符,从而找到相关段,因此一个段选择子唯一的标识了一个段。段描述符作为表述一个段的重要数据结构,涵盖了段资源方方面面的信息,如:段机制、段类型、访问权限、特权级别以及段大小。那么上面提到的段内偏移量(就是包含在逻辑地址中的第二部分)会被用来定位到段内的某个字节,即用这个偏移量加上段描述符中的段机制,相加后得到的值就是线性地址,这个地址总会落在处理器的线性地址空间内(32位就是4GB线性空间)。
如果没有启用分页机制,线性地址就会被直接映射到处理器的物理地址空间,所谓物理地址空间通常就是处理器在其地址总线上可以寻址的最大范围。
由于多任务系统通常会定义一段比实际物理地址大得多的线性地址空间,所以线性地址空间的虚拟化技术就非常关键了,而这种线性地址空间的虚拟化技术就要通过分页机制来实现。
分页提供了一种“虚拟内存”环境,用于仿真真实而紧缺的物理内存(RAM、ROM),甚至磁盘存储空间。当分页机制被启用后,每一个段都会被分割成同等大小的页面(典型页面大小为4KB),这些页面可能被存放在物理内存空间,也或许被存放在磁盘空间,这要视情况而定。操作系统或执行体(笔者认为这里提到的执行体是操作系统中具有高特权级的进程)维护着一张页目录以及一组页表,用于随时跟踪定位页面。当一个程序或任务试图访问线性地址空间中的某个地址时(线性地址),处理器就会使用页目录与页表,先将要访问的线性地址转换成物理地址,然后再为访问请求执行相关操作(读或写)。
如果要访问的页面没有存放在物理内存当中,处理器就会产生一个缺页异常,并终止当前程序的执行。随之,操作系统或执行体便会将希望访问的页面从磁盘空间换入物理内存,然后继续执行被打断的程序。
当分页机制被启用后,对于一个正确执行的程序来说,所有在磁盘空间与物理内存之间的页面交换过程都是透明的。请注意,即便是对于一个运行在虚拟8086模式下的16位程序而言,也可以透明的使用分页机制。
3.2 分段机制
IA-32提供的分段机制可广泛运用于操作系统设计之中,操作系统(简称系统)可以被设计为支持平坦模式(以段的方式保护多道程序间互不干扰),这也是一种最简单的分段方式,另外还可以被设计为多段模式,这种模式可以帮助实现一个健壮的系统,使多道程序能安全可靠的运行。
下面章节给出了几种分段机制在系统设计中的典型应用,让我们来看看怎样利用IA-32提供的分段机制来提高系统性能与可靠性。
3.2.1 基本平坦模式
系统中最简单的内存模式就是“基本平坦模式”,它使得操作系统与应用程序可以访问一段连续的,没有经过分段的地址空间。这种模式向系统设计者及应用程序设计者最大限度的隐藏了分段机制。
在IA-32中,要实现一个基本平坦内存模式,至少有两个段描述符需要被创建,一个用于代码段,一个用于数据段(见图3-2)。这两个段会被映射到整个线性地址空间中:这两个段描述符都具有相同的段基址(0),与相同的段边界(4GBytes),由于将段边界设置为4GB,所以即使被访问的内存地址超出了内存边界限制(笔者认为这里的边界是物理内存边界),或者要访问的内存地址上根本没有物理内存,段机制也不再会产生任何异常。ROM(EEPROM)通常被定位在物理地址空间的顶端,因为处理器加电后会在0xFFFFFFF0这个位置上开始执行。RAM(DRAM)被安排在地址空间的底端,因为处理器加电或复位后,数据段的基地址会被初始化为‘0’。
3.2.2 保护平坦模式
此模式与基本平坦模式很像,不同的是保护平坦模式会将段边界设置为实际物理内存的最大值(图3-3)。对于任何不在物理内存中的地址所进行的访问都将会导致一个通用保护异常的发生。针对各类程序中存在的缺陷,这种模式只提供了最小限度的硬件保护。
可以将一些更复杂的特性叠加于保护平坦模式之上,以提供更多的保护。例如,分页机制就在普通用户程序的代码段、数据段与特权级管理程序的代码段、数据段之间进行了很好的隔离,这其中有四个段是必要的:即普通用户程序的代码段与数据段(特权级别为‘3’),以及特权级管理程序的代码段与数据段(特权级别为‘0’)。通常情况下,这些段会相互覆盖,且在线性地址空间中的起始地址均为‘0’。这种保护平坦模式与分页机制一起工作,就可以很好的保护操作系统免受应用程序的侵扰,同时也可以为每一个任务或进程都进行分页,以在应用程序之间提供隔离保护,多任务操作系统通常都会采用这样的设计方案。
3.2.3 多段模式
多段模式(图3-4)运用了段机制的全部特性,以为程序的代码段、数据段及堆栈段提供硬件强制保护。在此模式中,所有的程序都会提供各自独立的段描述符表,以及各自独立的段。一个段只属于某一个程序,不过也可以在多道程序间共享。对于所有段资源的访问,以及系统中每一个程序执行环境的访问,全部是由硬件进行控制的。
在段的安全性检查方面,囊括了段寻址越界的检查,以及段操作越权的检查。例如,代码段是只读的,硬件就会针对其提供保护,以避免对代码段做写入操作。段访问的安检信息还可以用于为段建立保护环或保护级别,所谓保护级别,主要用于保护操作系统内的关键程序(笔者认为是一些内核态进程,以及拥有高特权级的超级管理员进程),避免普通用户程序对其进行未经授权的访问。
3.2.4 IA-32e模式的段
在前面提到过,Intel 64架构中提供的IA-32e模式还有两个不同的子模式(兼容模式与64位模式),而IA-32e在这两种不同的子模式中分别发挥着不同的作用。在兼容模式中,分段功能仅用于16位或32位保护模式。在64位模式中,分段在某种程度上被屏蔽(并不是真正的被禁止),以建立一个平坦的64位线性地址空间,处理器会将段寄存器CS、DS、ES、SS全置为零,使它们同落在一片相同的线性地址空间内,而GS与FS却不尽相同(以后介绍),而这些段寄存器则可以在线性地址空间的地址计算中发挥重要作用,它们可以用于数据寻址以及寻访操作系统中的一些关键数据结构。
要注意的是,在64位模式中,处理器是不会对段进行越界检查的。
3.2.4 IA-32e模式的段
在图3-2、3-3以及3-4描述的段模型中,全都可以启用分页机制,分页被启用后,处理器便会将线性地址空间(已经被分段的)分成相互独立的页面(如图3-1),而且会将这些页面与物理内存建立映射关系。分页机制提供了几种页面保护功能,主要是用于(或替换)段机制中的保护功能,它们分别是用于普通用户程序的保护级别,及管理程序的保护级别,这些保护级别可以在页面之间提供可靠的保护。
3.3 物理地址空间
在保护模式中,IA-32架构提供了最大32位的物理地址空间(即4GBytes),也是处理器在地址总线上可以寻址的最大空间范围。物理地址空间是平坦的(未分段),是一段从0到FFFFFFFFH的连续地址范围。物理地址空间还可以被映射成可读写内存、只读内存及I/O内存空间,而在这里提到的所谓映射就是指将物理内存分段或分页。
从Pentium Pro处理器开始,IA-32架构提供了一种可扩展的物理地址空间,长度达36位,即64GBytes,最大物理地址范围是FFFFFFFFFH。通过以下两个步骤即可启用这一特性:
- 设置控制寄存器CR4中的bit5(PAE标志),即启用物理地址扩展功能。
- 启用36位页面扩展特性(在Pentium3处理器中再加以介绍)。
要了解更多关于36位物理寻址方面的内容,请查阅章节3.8“使用PAE分页机制进行36位物理寻址”,与章节3.9“使用PSE-36分页机制进行36位物理寻址”。
3.3.1 Intel 64处理器及其物理地址空间
Intel 64架构处理器的物理地址范围,其定义比较特殊,使用一条关键指令来实现,即CPUID.80000008H:EAX[bits 7-0]。
要了解更多有关EAX返回值的信息,请查阅《Intel 64与IA-32架构软件开发者手册 卷二A》中的第三章“CPU 标识符 - CPUID”,及其章节3.8.1的“PAE分页机制的增强模式”。
3.4 逻辑地址与线性地址
在保护模式中,处理器为得到物理地址所做的地址转换被分为两个阶段,即逻辑地址转换(得到线性地址),以及对线性地址空间的分页(找到物理内存页)。
记得吗,即使是在最简单的段模式中,处理器地址空间中每一个字节的寻访也都是通过逻辑地址。逻辑地址包含了一个段选择子,和一个32位的段内偏移量(图3-5),段选择子用于定位段的位置,而段内偏移量则用于定位要寻访的段内地址。
处理器将逻辑地址转换为一个32位的线性地址,这个线性地址无疑会落在处理器的线性地址空间当中,和物理地址空间一样的是,线性地址空间也是平坦的(为分段),最大长度是32位,最大寻址范围是0到FFFFFFFFH,这个线性地址空间中包含了系统中所有的段资源及各种系统表。要将逻辑地址转换为线性地址,处理器要做以下几件事:
1、使用段选择子在GDT或LDT中找到相关段的段描述符,并加载关键信息(这个步骤发生在段选择子被加载进段寄存器之后)。
2、接着要依靠段描述符中提供的信息,对段访问进行权限检查与边界检查,以确保不会发生越权、越界。
3、将段描述符中提供的段基址与逻辑地址中的段内偏移量相加,继而得到一个32位的现行地址。
如果未启用分页,处理器便会将线性地址直接映射为物理地址,即是两个地址是一一对应的。如果启用了分页,那么便会产生地址转换的第二个阶段,即线性地址到物理地址的转换。
请参阅章节3.6“虚拟内存分页概述”
3.4.1 IA-32e模式的逻辑地址转换
在IA-32e模式的中(手册中没有提及是在其子模式,但笔者想应该是其第一种子模式,即兼容模式),Intel 64位处理器会用上面提到的方法,将逻辑地址转换为线性地址,而在64位模式中(也应该是IA-32e的另一种子模式,前面提到过),段基地与段内偏移量自然都不再使用32位,而会以64位取而代之。64位宽的线性地址格式是IA-32e的典型线性地址地址格式。
每一个代码段描述符都包含一个称为‘L’的位,这个位规定了代码段执行64位代码还是32位代码。
3.4.2 段选择子
每个段都有一个唯一的16位标识符,即段选择子,不过段选择子并不直接指向段,而是指向用于详细描述段信息的段描述符,段选择子包含了如下内容:
Index(索引)
Index共13位,占据了段选择子的bit3到bit15(笔者认为用英文单词描述位序列更准确),在GDT或LDT的8192个描述符中,就是通过这个索引找到相关段描述符的,处理器会先使Index乘八(段描述符的长度是8字节),然后再与GDT或LDT的基地址相加,继而定位到相关段描述符。
TI(表标识符)标志
只有一位,位于段选择子的bit2,它规定了到底使用哪个描述符表,将此位清零即选择了GDT(全局描述符表),设置此位(置‘1’)即选择了LDT(本地描述符表)。
RPL(请求特权级别)
共2位,占据着段选择子的bit0到bit1,它规定了段选择子的保护级别,保护级别的范围可以从0到3,0是最高保护级别。要了解更多保护级别的信息请查阅章节4.5,在哪里揭示了一个正在执行的程序(或任务),其RPL与CPL(当前保护级别)的关系,以及位于段描述符中的DPL(描述符保护级别)的概念。
GDT的第一项对处理器来说没有实际用处,当段选择子指向GDT的第一项时(即段选择子的Index为‘0’,TI标志也为‘0’),就会被作为一种称为“空段选择子”的东西来使用。当段寄存器(除了CS与SS)中加载了空段选择子时,处理器并不会因此产生异常,然而当要通过空段选择子去试图访问内存的时候,处理器就会产生异常。其实,空段选择子常常被用来初始化未使用的段寄存器。要注意的是,当CS或SS中加载了空段选择子时,处理器会立即产生异常。
段选择子对于应用程序是可见的,它会作为指针变量的一部份出现,而且选择子的值不是由应用程序分派或修改的,而是由程序连接器来完成。
3.4.3 段寄存器
为了减少地址转换时间,同时也为了降低代码复杂度,处理器提供了6个段寄存器,用于分别保存6个段选择子(图3-7),每个段寄存器,都专门用于某一特定类型的内存(如代码段、数据段、堆栈段),为了使程序正常运行,在CS、DS、SS三个段寄存器中,至少要有一个加载了段选择子。处理器还为当前正在执行的程序(或任务)提供了三个附加数据段,它们的段选择子分别保存于ES、FS和GS中。
前面讲过,要访问一个段,就要将其段选择子装入相关段寄存器,可处理器仅仅提供了6个段寄存器,而我们的系统中可以定义几千个段,所以可被立即使用的段选择子也只有6个而已,如果要在程序(或任务)执行期间使用更多其它的段,就要把它们的段选择子装入处理器的某个段寄存器中才行。
每个段寄存器都包含两大部分,即“可见部分”与“隐藏部分”(隐藏部分有时也被称作“描述符缓冲器”或“影子寄存器”),段选择子会被装入段寄存器的可见部分,与此同时处理器会将段描述符中的段基址、段边界以及段访问控制信息,装入段寄存器的隐藏部分。要读取段描述符中的段基址、段边界等信息需要寻访内存,而将这些信息缓存在段寄存器中则会大大提高处理器的地址转换速度,避免了过多的内存访问,减少了很多额外的总线周期。我们已经知道,段寄存器中缓存着段描述符中的重要信息,而在多处理器系统中,描述符表随时可能被某个处理器改变,因此需要由软件负责更新段寄存器中的相关信息,以保证持数据是最新的。
系统提供了两类段寄存器的装载指令:
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做地址运算的时候,段基址会被视为零。
在此模式中,处理器不会再做段边界检查,而是对所有线性地址做标准格式检查,模式交换不会改变段寄存器的内容,也不需要寻访描述符寄存器,因此ES、DS、SS在64位模式运行期间不会被改变,除非显示的对它们执行装载动作。
3.4.5 段描述符
段描述符,用于为处理器提供段的各类重要信息(如段基址、段边界,段访问控制状态及状态信息等),以特定数据结构保存于GDT或LDT中。段描述符通常是由编译器、连接器或装载程序创建,也可以由操作系统或执行体来创建,但应用程序是无法创建段描述符的。图3-8给出了大多数段描述符所使用的通用描述符格式。
下面就对段描述符中的内容进行逐项分析:
段边界 - segment limit
段边界规定了一个段的大小,处理器会将描述符中存在的两部分段边界,组合成一个20位的值,而处理器对这个值的解释,还要依赖于描述符中另外一项内容,即G标志(粒度):
如果‘G’标志被清零,段大小范围则被限定在1字节到1兆字节之间,并以字节为一个增长单位(如在段内寻址下一个位置时,便增加一个字节)。
如果‘G’标志被设置(置‘1’),段大小范围则从4KBytes到4GBytes之间,并以4KBytes为一个增长单位(如在段内寻址下一个位置时,便增加4KBytes)。
处理器对段边界的使用,要区分两种不同的情况,即段是向上增长的还是向下增长的(要了解更多关于段类型的信息,请查阅章节3.4.5.1“代码段与数据段的描述符类型”)。对于向上增长的段,逻辑地址中偏移量的范围可以从0一直到段边界,大于段边界的偏移量会使处理器产生通用保护异常(#GP)。对于向下增长的段,段在边界位置上有一块保留区域(长度为64KBytes),因此逻辑地址中的偏移量范围是从FFFFFFFFH到FFFFH,不过这还要看‘B’标志是怎样设置的而定(后面会介绍),当偏移量小于段边界(即FFFFH)时,处理器便会产生通用保护异常,在向下增长的段中,对段内地址进行递减,会在段地址空间的底部定位(或分配)内存,相反,在向上增长的段中,则是在顶部。在IA-32架构中,堆栈段就是向下增长的,这非常有利于堆栈的扩展。
基地址 - base address
基地址,即段在4GBytes线性地址空间中的‘0’字节位置。在描述符中,有三个区域都分别保存着基地址数据,处理器会将这三部分拼在一起,形成一个32位的段基址值,段基址应该在16字节边界位置上进行对齐,虽然16字节对齐不是必须的,但当代码段与数据段都在16字节边界位置上对齐时,程序执行的效率会显著提高。
类型 - Type
定义了段或门的类型,还规定了段的访问类别及增长方向(向下或向上)。对于此类型域的解释,还要视描述符类型标志(即‘S’标志)怎样设置而定(下面会做解释)。对于此类型域的设置,代码段与数据段之间是有区别的(参见图4-1),要了解如何定义代码段与数据段的类型,请参见章节3.4.5.1“代码段与数据段的描述符类型”。
S(描述符类型)标志
此标志规定了段描述符是要用于系统段(S标志被清零)的描述,还是要用于普通程序的代码段或数据段的描述(S标志被设置)。
DPL (描述符特权级别)
DPL规定了段的特权级别,特权级可以从‘0’到‘3’,0是最高特权级,DPL用于段的访问控制。在代码段DPL、CPL,及段选择子的RPL之间存在着种种关系,要了解这方面内容,请参见章节4.5“保护级别”。
P(段出现)标志
此标志被设置,则表示段在内存中,反之则表示段不在内存中。一旦此标志被清零,那么此时若再向段寄存器中装入指向段描述符的段选择子,便会使处理器产生一个异常(#NP),即表示段不在内存中。内存管理软件可以利用此标志来控制段,以在特定时间点将段换入物理内存。此标志在虚拟内存管理的分页功能中,也起着关键的控制作用(以后会做详述)。
图3-9描述的是当‘P’标志被清零时,段描述符的格式。我们可以看到有很大区域被标记为‘Available’,操作系统或执行体可以自由的使用这块区域,用于存储它们自己的数据,例如未在物理内存中出现的段地址信息。
D/B 标志
此标志的作用,要视段描述符的几种不同使用情况而定,是用于可执行代码段、向下扩展的数据段,还是堆栈段,下面我们就逐一分析这几种不同情况。(当使用32位的代码段与数据段时,这个标志需要被设置为‘1’,使用16位代码段与数据段时,则设置为‘0’)
- 可执行代码段 - 在这里,D/B标志将以‘D’标志的身份出现(笔者注:想必是Default的首字母),它规定了段中有效地址的默认长度,及指令操作数的默认长度。若设置此标志(置1),则表示使用32位地址及32位(也可以是8位)操作数,若此位被清零,则表示使用16位地址及16位(也可以是8位)操作数。另外使用指令前缀码也可以改变默认地址长度与操作数长度,指令前缀码66H可用于设定操作数长度,指令前缀码67H可用于设定地址长度。
- 堆栈段(由SS寄存器指向的数据段) - 在堆栈段中,D/B标志将以‘B’标志的身份出现(笔者注:想必是Bound的首字母),它规定了用于隐式段操作(如pushes、pops及 calls)的段指针的长度。若设置此标志,则表示使用32位栈指针(位于32位的ESP寄存器中),若将此标志清零,则表示使用16位栈指针(位于16 位的SP寄存器中)。若堆栈段被设定为向下扩展的数据段,则此标志还可用于定义堆栈段的最大边界值。
- 向下扩展的数据段 - D/B标志仍将以‘B’标志的身份出现,它规定了段的最大边界值,若设置此标志,则段的最大边界为FFFFFFFFH(4GBytes),若将此标志清零,则段的最大边界为FFFFH(64KBytes)。
G(粒度)标志
定义了段边界的变化粒度,即当此标志被清零时,段边界以字节为单位增加或减少,若设置此标志,则段边界以4KB为单位增加或减少。举个例子,若设置了此标志,则进行段边界检查(是否越界)时,就不会检查段内偏移量的低12位,即偏移量的值在0~4095之间都被认为是合法的(4KB)。最后要注意,这个标志不会影响段基址的变化粒度,段基址永远以字节为单位。
L(64位代码段)标志
在IA-32e模式中,此标志位规定了代码段是否包含64位代码,即当此位置1时,代码段将运行于64位模式,当此位清零,则表示代码段运行于兼容模式(笔者注:请参见前面章节关于IA-32e子模式的描述)。当此位被设置时,‘D’标志必须被清零,此位只在IA-32e模式及代码段中有效,其他情况下均用作保留位,或被置为0。
保留位
描述符第二部分的bit20作为保留位,可由系统软件使用。
3.4.5.1 代码段与数据段的描述符类型
前面提过,描述符中的‘S’标志规定了段描述符的类型,当此标志位被设置时,就表示段描述符用于描述代码段或数据段,进一步则可以通过类型域的最高位(描述符第二部分的bit11)来区分是用于代码段(bit11清零)还是用于数据段(bit11置位)。
对于数据段,类型域的最低三位(段描述符第二部分的bit8~bit10)可以被解释为段的访问记录(A)、写使能(W)或段扩展方向(E)。表3-1分别列出了代码段与数据段的类型域编码数值(低三位编码),先看表上面的数据段部分,它既可以是只读的,也能设定为可读写,这要看写使能位是如何设定的(请注意bit8、9、10分别标注了A、W、E,即表示访问权限、写使能、段扩展方向)。
堆栈段即是一种可读写的数据段,当向SS寄存器中装载一个指向“不可写数据段”的段选择子时,便会导致处理器产生通用保护异常(#GP)。如果堆栈段的大小需要动态调整,此时就可将堆栈段视为一个向下扩展的数据段(需要将‘E’标志位置1),之后动态调整栈边界(如增加)就会使栈空间在最底部位置上向下扩展。如果堆栈段的长度打算保持为静态不变的,则它既可以是向上扩展的,也可以是向下扩展的。
“访问位”(即bit8)标识了自操作系统或执行体最后一次将此位清零以来,段是否被再一次访问过。只要向段寄存器中装入段选择子,处理器便会立刻设置此位,前提是包含段描述符的内存区域是可写的,对此位的设置会一直保持不变,除非对其进行显式的清零。这个位还可以用于虚拟内存管理与调试。
继续看表的下半部分代码段,此时类型域的低三位被可被解释成访问记录(A)、读使能(R)或一致性(C)。代码段可以是“只执行”的,也可以是“可读/可执行”的,这要看“读使能(R)”是怎样设置的而定。一个“可读/可执行”的代码段常用于代码与数据共存一处的情况(例如某些常量或其它静态数据与指令代码被一同安置在ROM中),对于这种情况,数据要从代码段中读取出来可以有两种方式,一是通过访内指令,对带有CS前缀码的内存区域做读取操作(如 MOV CS:XXXX),另外也可以将代码段的段选择子装入数据段寄存器(如DS、ES、FS或GS寄存器)。在保护模式中,代码段是不可写的。
代码段既可以是“一致的”,也可以是“非一致的”。其实这两个概念理解起来并不难,一个高特权级别的一致性代码段(即代码段是“一致的”),允许某个代码段特权级别比它低的执行程序进入(指访问或跳转到)它并继续执行,而在非一致性代码段(即代码段是“非一致的”)中,此类操作则会导致通用保护异常(#GP)的发生,除非使用调用门或任务门(要了解更多关于代码段一致性的知识,请查阅章节4.8.1“直接调用或跳转到代码段”)。某些类型的异常(除法错误或溢出)通常都会有相关的处理例程,这些处理例程是受保护的,而那些不会访问此类处理例程(或其它受保护的系统功能)的系统实用程序,是不会对受保护的系统功功能或处理例程构成威胁的,因此便可以被安置在一致性代码段中,以使更低特权级别的程序可以对其进行访问。而某些系统程序则要提供特别的保护,以避免低特权级程序对其构成威胁,这些关键系统程序需要被安置在非一致性代码段中。
注意:执行程序不能调用或跳转入一个保护级别低于它的代码段(特权级数值较高的,如‘3’),不管这个目标段是一致的还是非一致的,试图做这样的操作都将会导致通用保护异常的发生。
所有的数据段都是非一致性的,即它们不能被特权级别低于自己的程序访问(执行代码的特权级数值较高),然而与代码段不同的是,数据段可以不必通过门就可以被更高特权级的程序访问(执行代码的特权级数值较低,如‘0’)。
如果GDT或LDT中的某个段描述符被安置在ROM中,则一旦处理器或软件呢试图去更新ROM中的段描述符时,就会陷入一个死循环(笔者注:一定是因为描述符中的“访问记录”是零,又因为ROM是不可写的,无法改变这个标志位的值,所以会进入试图更新它的死循环)。为了避免这个问题,需要将ROM中段描述符的“访问记录”标志置‘1’(笔者注:由于ROM是不可写的,因此一定是在操作系统启动以前,使用特别方式设置的),而且还需要移除任何试图修改ROM中段描述符的操作系统(或执行体)代码。
3.5 系统描述符类型
当段描述符中的‘S’标志被清零时,描述符类型就被定义为系统描述符,处理器可识别出以下类型的系统描述符:
- 本地描述符表(LDT)的段描述符
- 任务状态段(TSS)描述符
- 调用门描述符
- 中断门描述符
- 陷阱门描述符
- 任务门描述符
这些描述符被归结为两类:即系统段描述符及门描述符。系统段描述符指向了系统的段(LDT与TSS段)。门描述符即是一道门,通常包含了代码段相关处理程序的入口指针(如调用门、中断门及陷阱门),或包含了TSS段的段选择子(如任务门)。
表3-2给出了系统段描述符及门描述符中类型域的编码组合。注意:IA-32e模式的系统描述符(包括系统段描述符与门描述符)是16字节的,而非8字节(IA-32架构是8字节)。
要了解更多关于系统段描述符的信息,请参阅章节3.5.1“段描述符表”,及章节6.2.2“TSS描述符”。了解更多关于门描述符的信息,请参阅章节4.8.3“调用门”,及章节5.11“IDT描述符”与章节6.2.5“任务门描述符”。
3.5.1 段描述符表
段描述符表即是一个描述符数组(如图3-10),其长度是可变的,最多可以容纳8192个描述符(每个描述符8字节)。前面提过有两种类型的描述符表:
每个系统中都必须定一个GDT,由系统中所有程序及任务共同使用,而LDT是可选的,可以有一个也可以有多个,每个独立任务都可以拥有自己的LDT,而一个LDT也可以在多个任务间共享。
GDT本身并不是一个段,它只是线性地址空间中的一个数据结构,GDT的基地址与边界值必须被加载到GDTR寄存器中(参见章节2.4“内存管理寄存器”)。GDT的基地址应该在8字节边界位置上对其,以使处理器发挥最佳性能。GDT的长度值是以字节为单位的,与段类似的是,要得到最大有效地址字节数则需要将其边界值与基地址相加。由于每一个段描述符都是8字节长,因此GDT的边界值应该始终比8的整数倍小一(如:若GDT是16字节长,则边界值应该是15,因为是0~15)。
处理器没有使用GDT中的第一个描述符(也被称为“空描述符”),虽然向段寄存器(DS、ES、FS、GS)中装入指向空描述符的段选择子不会导致异常的发生,但是试图利用空描述符去访问内存则会导致通用保护异常(#GP)的发生。前面提过通常用指向空描述符的段选择子初始化段寄存器,之所以这样做原因很简单,就是为了在使用未做有效赋值的段寄存器时,系统会产生通用保护异常,而非坐视不管。
LDT被定位于系统段中,GDT中保存着LDT段的段描述符,如果系统中拥有多个LDT,则每一个LDT段都会拥有独立的段选择子,并在GDT中保存着它们各自独立的段描述符(在GDT的任何位置上均可安置LDT段描述符)。要了解关于LDT段描述符的类型,请参见章节3.5“系统描述符类型”。
LDT是通过段选择子进行访问的,在访问时,为了减少地址转换的时间开销,通常将LDT的段选择子、线性基地址、段边界、访问权限都保存在LDTR寄存器中(请参见章节2.4“内存管理寄存器”)。
当使用SGDT指令保存GDTR寄存器的内容时(关于SGDT,请参阅“指令集手册”),其实等于将一个48位的“伪描述符”(笔者注:这里之所以提到伪描述符,笔者认为是由于GDTR中的内容包含了一个真正描述符的关键信息,相当于一个描述符,但又不是真正的描述符)存放在内存中(如图3-11上半部)。为了避免用户模式(特权级别为‘3’)的对齐检查失败,伪描述符应该被安置在一个奇数WORD地址上(即地址模4等于2),这会使处理器在此地址上存储一个对齐的字(WORD,即GDTR的前16位limit信息),然后紧跟着再存储一个对齐的双字(DoubleWord,4个字节,即GDTR中的后32位基地址信息)。用户模式的程序通常不会做存储伪描述符的动作,但是通过上述方法确实可以避免对齐检查失败的异常。同样,当使用SIDT指令保存IDTR寄存器时也需要对齐,还有,当使用SLTR或STR指令保存LDTR或TR寄存器时,伪描述符需要被安置在双字地址上(即地址模4等于0)。之所以做了这些对齐操作,都是因为处理器是32位的,需要在4字节地址上存取数据,当数据做了对齐操作后,存取效率就会提升,每一次存取就可以直接拿到希望得到的数据,而无需再进一步进行拼凑组合。
3.5.2 IA-32e模式的段描述符
在IA-32e模式中,段描述符表最多可以包含8192个描述符,段描述符表的每一项都是8字节长,而系统描述符被扩展成16字节长,但实际上这是通过占用两个描述符表项来实现的。
GDTR与LDTR也得到了扩展,即它们各自都可以包含一个64位的基地址,其伪描述符长度可达80位(参见图8-11下半部分)。
下面列出的系统描述符的长度都被扩展成16字节,它们是:
- 调用门描述符(参见章节4.8.3.1“IA-32e模式的调用门”)
- IDT门描述符(参见章节5.14.1“64位模式的IDT”)
- LDT与TSS描述符(参见章节6.2.3“64位模式的TSS描述符”)
转载于:https://blog.51cto.com/lion3875/532337