Linux0.11 80X86知识(七)(上):https://developer.aliyun.com/article/1597265
4、系统指令
四、保护模式内存管理
1、内存寻址
内存是指一组有序字节组成的数组,每个字节有唯一的内存地址。内存寻址则是指对存储在内存中的某个指定数据对象的地址进行定位。这里,数据对象是指存储在内存中的一个指定数据类型的数值或字符串。80X86 支持多种数据类型:1 字节、2 字节(1 个字)或 4 字节(双字或长字)的无符号整型数或带符号整型数,以及多字节字符串等。通常字节中某一比特位的定位或寻址可以基于字节来寻址,因此最小数据类型的寻址是对 1 字节数据(数值或字符)的定位。通常内存地址从 0 开始编址,对于 80X86 CPU 来说,其地址总线宽度为 32 位,因此一共有 2^32 个不同物理地址。即内存物理地址空间有 4G,总共可以寻址 4G 字节的物理内存。对于多字节数据类型(例如 2 字节整数数据类型),在内存中这些字节相邻存放。 80X86 首先存放低值字节,随后地址处存放高值字节。因此 80X86 CPU 是一种先存小值(Little Endium)的处理器。
对于 80X86 CPU,一条指令主要由操作码(Opcode)和操作对象即操作数(Oprand)构成。操作数可以位于一个寄存器中,也可以在内存中。若要定位内存中的操作数,就要进行内存寻址。80X86 有许多指令的操作数涉及内存寻址,并且针对所寻址对象数据类型的不同,也有很多不同的寻址方案可供选择。为了进行内存寻址, 80X86 使用了一种称为 段(Segment)的寻址 技术。这种寻址技术把内存空间分成一个或多个称为段的线性区域,从而对内存中一个数据对象的寻址就需要使用一个段的起始地址(即 段地址 )和一个 段内偏移地址 两部分构成。段地址部分使用 16 位的 段选择符 指定,其中 14 位可以选择 2^14 次方即 16384 个段。段内偏移地址部分使用 32 位的值来指定,因北段内地址可以是 0 到 4G。即一个段的最大长度可达 4G。程序中由 16 位的段和 32 位的偏移构成的 48 位地址或长指针称为一个逻辑地址(虚拟地址)。它唯一确定了一个数据对象的段地址和段内偏移地址。而仅由 32 位偏移地址或指针指定的地址是基于当前段的对象地址。
80X86 为段部分提供了 6 个存放段选择符的段寄存器:CS、DS、ES、SS、FS 和 GS。其中 CS 总是用于寻址代码段,而堆栈段则专门使用 SS 段寄存器。在任何指定时刻由 CS 寻址的段称为当前代码段。此时 EIP 寄存器中包含了当前代码段内下一条要执行指令的段内偏移地址。因此要执行指令的地址可表示成 CS:[EIP] 。后面将说明的段间控制转移指令可以被用来为 CS 和 EIP 赋予新值,从而可以把执行位置改变到其他的代码段中,这样就实现了在不同段中程序的控制传递。
由段寄存器 SS 寻址的段称为当前堆栈段。栈顶由 ESP 寄存器内容指定。因此堆栈顶处地址是 SS:[ESP] 。另外 4 个段寄存器是通用段寄存器。当指令中没有指定所操作数据的段时,那么 DS 将是默认的数据段寄存器。为了指定内存操作数的段内偏移地址,80X86 指令规定了计算偏移量的很多方式,称为指令寻址方式。指令的偏移量由三部分相加组成:基地址寄存器、变址寄存器和一个偏移常量。即:
偏移地址 = 基地址 + (变址 * 比例因子) + 偏移量
2、地址变换
任何完整的内存管理系统都包含两个关键部分:保护和地址变换。提供保护措施是可以防止一个任务访问另一个任务或操作系统的内存区域。地址变换能够让操作系统在给任务分配内存时具有灵活性,并且因为我们可以让某些物理地址不被任何逻辑地址所映射,所以在地址变换过程中同时也提供了内存保护功能。
正如上面提到的,计算机中的物理内存是字节的线性数组,每个字节具有一个唯一的物理地址;程序中的地址是由两部分构成的逻辑地制。这种逻辑地址并不能直接用于访问物理内存,而需要使用地址变换机制将它变换或映射到物理内存地址上。内存管理机制即用于将这种逻辑地址转换成物理内存地址。
为了减少确定地址变换所需要的信息,变换或映射通常以内存块作为操作单位。分段机制和分页机制是两种广泛使用的地址变换技术。它们的不同之处在于逻辑地址是如何组织成被映射的内存块、变换信息如何指定以及编程人员如何进行操作。分段和分页操作都使用驻留在内存中的表来指定它们各自的变换信息。这些表只能由操作系统访问,以防止应用程序擅自修改。
80X86 在从逻辑地址到物理地址变换过程中使用了分段和分页两种机制,见图 4-4 所示。第一阶段使用分段机制把程序的逻辑地址变换成处理器可寻址内存空间(称为线性地址空间)中的地址。第二阶段使用分页机制把线性地址转换为物理地址。在地址变换过程中,第一阶段的分段变换机制总是使用的,而第二阶段的分页机制则是供选用的。如果没有启用分页机制,那么分段机制产生的线性地址空间就直接映射到处理器的物理地址空间上。物理地址空间定义为处理器在其地址总线上能够产生的地址范围。
- 分段机制
分段提供了隔绝各个代码、数据和堆栈区域的机制,因此多个程序(或任务)可以运行在同一个处理器上而不会互相干扰。分页机制为传统需求页、虚拟内存系统提供了实现机制。其中虚拟内存系统用于实现程序代码按要求被映射到物理内存中。分页机制当然也能用于提供多任务之间的隔离措施。
如图 4-5 所示,分段提供了一种机制,用于把处理器可寻址的线性地址空间划分成一些较小的称为段的受保护地址空间区域。段可以用来存放程序的代码、数据和堆栈,或者用来存放系统数据结构(例如 TSS 或 LDT)。如果处理器中有多个程序或任务在运行,那么每个程序可分配各自的一套段。此时处理器就可以加强这些段之间的界,并且确保一个程序不会通过访问另一个程序的段而干扰程序的执行。分段机制还允许对段进行分类。这样,对特定类型段的操作能够受到限制。
一个系统中所有使用的段都包含在处理器线性地址空间中。为了定位指定段中的一个字节,程序必须提供一个逻辑地址。逻辑地址包括一个段选择符和一个偏移量。段选择符是一个段的唯一标识。另外,段选择符 提供了 段描述符表(例如全局描述符表 GDT)中一个 数据结构 (称为段描述符)的 偏移量 。每个段都有一个段描述符。段描述符指明段的大小、访问权限和段的特权级、段类型以及段的第 1 个字节在线性地址空间中的位置(称为段的基地址)。逻辑地址的偏移量部分加到段的基地址上就可以定位段中某个字节的位置。因此基地址加上偏移量就形成了处理器线性地址空间中的地址。
线性地址空间与物理地址空间具有相同的结构。相对于两维的逻辑地址空间来说,它们两者都是一维地址空间。虚拟地址(逻辑地址)空间可包含最多 16K 的段,而每个段最长可达 4GB,使得虚拟地址空间容量达到 64TB(246)。线性地址空间和物理地址空间都是 4GB(232)。实际上,如果禁用分页机制,那么线性地址空间就是物理地址空间。
2.分页机制
因为多任务系统通常定义的线性地址空间都要比其含有的物理内存容量大得多,所以需要使用某种"虚拟化"线性地址空间的方法,即使用虚拟存储技术。虚拟存储是一种内存管理技术,使用这种技术可让编程人员产生内存空间要比计算机中实际物理内存容量大很多的错觉。利用这种错觉,我们可以随意编制大型程序而无需考虑实际物理内存究竟有多少。
分页机制支持虚拟存储技术。在使用虚拟存储的环境中,大容量的线性地址空间需要使用小块的物理内存(RAM 或 ROM)以及某些外部存储空间(例如大容量硬盘)来模拟。当使用分页时,每个段被划分成页面(通常每页为 4KB 大小),页面会被存储于物理内存中或硬盘上。操作系统通过维护一个页目录和一些页表来留意这些页面。当程序(或任务)试图访问线性地址空间中的一个地址位置时,处理器就会使用页目录和页表把线性地址转换成一个物理地址,然后在该内存位置上执行所要求的操作(读或写)。
如果当前被访问的页面不在物理内存中,处理器就会中断程序的执行(通过产生一个页错误异常)。然后操作系统就可以从硬盘上把该页面读入物理内存中,并继续执行刚才被中断的程序。当操作系统严格实现了分页机制时,那么对于正确执行的程序来说页面在物理内存和硬盘之间的交换就是透明的。
80X86 分页机制最适合支持虚拟存储技术。分页机制会使用大小固定的内存块,而分段管理则使用了大小可变的块来管理内存。无论在物理内存中还是在硬盘上,分页使用固定大小的块更为适合管理物理内存。另一方面,分段机制使用大小可变的块更适合处理复杂系统的逻辑分区。可以定义与逻辑块大小适合的内存单元而无需受到固定大小页面的限制。每个段都可以作为一个单元来处理,从而简化了段的保护和共享操作。
分段和分页是两种不同的地址变换机制,它们都对整个地址变换操作提供独立的处理阶段。尽管两种机制都使用存储在内存中的变换表,但所用的表结构不同。实际上,段表存储在线性地址空间,而页表则保存在物理地址空间。因而段变换表可由分页机制重新定位而无需段机制的信息来合作。段变换机制把虚拟地址(逻辑地址)变换成线性地址,并且在线性地址空间中访问自己的表,但是并不知晓分页机制把这些线性地址转换到物理地址的过程。类以地,分页机制也不知道程序产生地址的虚拟地址空间。分页机制只是简单地把线性地址转换成物理地址,并且在物理内存中访问自己的转换表。
3、保护
80X86 支持两类保护。其一是通过给每个任务不同的虚拟地址(逻辑地址)空间来完全隔离各个任务。这是通过给每个任务逻辑地址到物理地址不同的变换映射来做到。另一个保护机制对任务进行操作,以保护操作系统内存段和处理器特殊系统寄存器不被应用程序访问。
1.任务之间的保护
保护的一个重要方面是提供应用程序各任务之间的保护能力。80X86 使用的方法是通过把每个任务放置在不同的虚拟地址空间中,并给予每个任务不同的逻辑地址到物理地址的变换映射。每个任务中的地址变换功能被定义成一个任务中的逻辑地址映射到物理内存的一部分区域,而另一个任务中的逻辑地址映射到物理内存中的不同区域中。这样,因为一个任务不可能生成能够映射到其他任务逻辑地址对应使用的物理内存部分,所以所有任务都被隔绝开了。只需给每个任务各自独立的映射表,每个任务就会有不同的地址变换函数。在 80X86 中,每个任务都有自己的段表和页表。当处理器切换去执行一个新任务时,任务切换的关键部分就是切换到新任务的变换表。
通过在所有任务中安排具有相同的虚拟到物理地址映射部分,并且把操作系统存储在这个公共的虚拟地址空间部分,操作系统可以被所有任务共享。这个所有任务都具有的相同虚拟地址空间部分被称为全局地址空间(Global address space)。这也正是现代 Linux 操作系统使用虚拟地址空间的方式。
每个任务唯一的虚拟地址空间部分被称为局部地址空间(Local address space)。局部地址空间含有需要与系统中其他任务区别开的私有的代码和数据。由于每个任务中具有不同的局部地址空间,因此两个不同任务中对相同虚拟地址处的引用将转换到不同的物理地址处。这使得操作系统可以给与每个任务的内存相同的虚拟地址,但仍然能隔绝每个任务。另一方面,所有任务在全局地址空间中对相同虚拟地址的引用将被转换到同一个物理地址处。这给公共代码和数据(例如操作系统)的共享提供了支持。
2.特权级保护
在一个任务中,定义了 4 个执行特权级(Privilege Levels),用于依据段中含有数据的敏感度以及任务中不同程序部分的受信程度,来限制对任务中各段的访问。最敏感的数据被赋予了最高特权级,它们只能被任务中最受信任的部分访问。不太敏感的数据被赋予较低的特权级,它们可以被任务中较低特权级的代码访问。
特权级用数字 0 到 3 表示,0 具有最高特权级,而 3 则是最低特权级。每个内存段都与一个特权级相关联。这个特权级限制具有足够特权级的程序来访问一个段。我们知道,处理器从 CS 寄存器指定的段中取得和执行指令,当前特权级(Current Privilege Level),即 CPL 就是当前活动代码段的特权级,并且它定义了当前所执行程序的特权级别。CPL 确定了哪些段能够被程序访问。
每当程序企图访问一个段时,当前特权级就会与段的特权级进行比较,以确定是否有访问许可。在给定 CPL 级别上执行的程序允许访问同级别或低级别的数据段。任何对高级别段的引用都是非法的,并且会引发一个异常来通知操作系统。
每个特权级都有自己的程序栈,以避免使用共享栈带来的保护问题。当程序从一个特权级切换到另一个特权级上执行时,堆栈段也随之改换到新级别的堆栈中。
五、分段机制
分段机制可用于实现多种系统设计。这些设计范围从使用分段机制的最小功能来保护程序的平坦模型,到使用分段机制创建一个可同时可靠地运行多个程序(或任务)的具有稳固操作环境的多段模型。
多段模型能够利用分段机制全部功能提供由硬件增强的代码、数据结构、程序和任务的保护措施。通常,每个程序(或任务)都使用自己的段描述符表以及自己的段。对程序来说段能够完全是私有的,或者是程序之间共享的。对所有段以及系统上运行程序各自执行环境的访问都由硬件控制。
访问检查不仅能够用来保护对段界限以外地址的引用,而且也能用来在某些段中防止执行不允许的操作。例如,因为代码段被设计成是只读形式的段,因此可以用硬件来防止对代码段执行写操作。段中的访问权限信息也可以用来设置保护环或级别。保护级别可用于保护操作系统程序不受应程序非法访问。
1、段的定义
在上一节概述中已经提到,保护模式中 80X86 提供了 4GB 的物理地址空间。这是处理器在其地址总线上可以寻址的地址空间。这个地址空间是平坦的,地址范围从 0 到 0xFFFFFFFF。这个物理地址空间可以映射到读写内存、只读内存以及内存映时 I/O 中。分段机制就是把虚拟地址空间中的虚拟内存组织成一些长度可变的称为段的内存块单元。80386 虚拟地址空间中的虚拟地址(逻辑地址)由一个段部分和一个偏移部分构成。段是虚拟地址到线性地址转换机制的基础。每个段由三个参数定义:
- 段基地址(Base address),指定段在线性地址空间中的开始地址。基地址是线性地址,对应于段中偏移 0 处。
- 段限长(limit),是虚拟地址空间中段内最大可用偏移位置。它定义了段的长度。
3.段属性((Attributes),指定段的特性。例如该段是否可读、可写或可作为一个程序执行;段的特权级等。
段限长定义了在虚拟地址空间中段的大小。段基址和段限长定义了段所映射的线性地址范围或区域。段内 0 到 limit 的地址范围对应线性地址中范围 base 到 base + limit。偏移量大于段限长的虚拟地址是无意义的,如果使用则会导致异常。另外,若访问一个段并没有得到段属性许可则也会导致异常。例如,如果你试图写一个只读的段,那么 80386 就会产生一个异常。另外,多个段映射到线性地址中的范围可以部分重叠或覆盖,甚至完全重叠,见图 4-6 所示。在本书介绍的 Linux 0.1x 系统中,一个任务的代码段和数据段的段限长相同,并被映射到线性地址完全相同而重叠的区域上。
段的基地址、段限长以及段的保护属性存储在一个称为段描述符(Segment Descriptor)的结构项中。在逻辑地址到线性地址的转换映射过程中会使用这个段描述符。段描述符保存在内存中的段描述符表(Descriptor table)中。段描述符表是包含段描述符项的一个简单数组。前面介绍的段选择符即用于通过指定表中一个段描述符的位置来指定相应的段。
即使利用段的最小功能,使用逻辑地址也能访问处理器地址空间中的每个字节。逻辑地址由 16 位的段选择符和 32 位的偏移量组成,见图 4-7 所示。段选择符指定字节所在的段,而偏移量指定该字节在段中相对于段基地址的位置。处理器会把每个逻辑地址转换成线性地址。线性地址是处理器线性地址空间中的 32 位地址。与物理地址空间类似,线性地址空间也是平坦的 4GB 地址空间,地址范围从 0 到 0xFFFFFFFF。线性地址空间中含有为系统定义的所有段和系统表。
为了把逻辑地址转换成一个线性地址,处理器会执行以下操作:
- 使用段选择符中的偏移值(段索引)在 GDT 或 LDT 表中定位相应的段描述符。(仅当一个新的段选择符加载到段寄存器中时才需要这一步。)
- 利用段描述符检验段的访问权限和范围,以确保该段是可访问的并且偏移量位于段界限内。
- 把段描述符中取得的段基地址加到偏移量上,最后形成一个线性地址。
图 4-7 逻辑地址到线性地址的变换过程
如果没有开启分页,那么处理器直接把线性地址映射到物理地址(即线性地址被送到处理器地址总线上)。如果对线性地址空间进行了分页处理,那么就会使用二级地址转换把线性地址转换成物理地址。页转换将在稍后进行说明。
2、段描述符表
段描述符表是段描述符的一个数组,见图 4-8 所示。描述符表的长度可变,最多可以包含 8192 个 8 字节描述符。有两种描述符表:全局描述符表 GDT(Global descriptor table);局部描述符表 LDT(Localdescriptor table)。
描述符表存储在由操作系统维护着的特殊数据结构中,并且由处理器的内存管理硬件来引用。这些特殊结构应该保存在仅由操作系统软件访问的受保护的内存区域中,以防止应用程序修改其中的地址转换信息。虚拟地址空间被分割成大小相等的两半。一半由 GDT 来映射变换到线性地址,另一半则由 LDT 来映射。整个虚拟地址空间共含有 214 个段:一半空间(即 213 个段)是由 GDT 映射的全局虚拟地址空间,另一半是由 LDT 映射的局部虚拟地址空间。通过指定一个描述符表(GDT 或 LDT)以及表中描述符号,我们就可以定位一个描述符。
当发生任务切换时,LDT 会更换成新任务的 LDT,但是 GDT 并不会改变。因此,GDT 所映射的一半虚拟地址空间是系统中所有任务共有的,但是 LDT 所映射的另一半则在任务切换时被改变。系统中所有任务共享的段由 GDT 来映射。这样的段通常包括含有操作系统的段以及所有任务各自的包含 LDT 的特殊段。LDT 段可以想象成属于操作系统的数据。
图 4-9 示出一个任务中的段如何能在 GDT 和 LDT 之间分开。图中共有 6 个段,分别用于两个应用程序(A 和 B)以及操作系统。系统中每个应用程序对应一个任务,并且每个任务有自己的 LDT。应用程序 A 在任务 A 中运行,拥有 LDTA,用来映射段 CodeA 和 DataA。类似地,应用程序 B 在任务 B 中运行,使用 LDTB 来映射 CodeB 和 DataB 段。包含操作系统内核的两个段 Codeos 和 Dataos 使用 GDT 来映射,这样它们可以被两个任务所共享。两个 LDT 段:LDTA 和 LDTB 也使用 GDT 来映射。
当任务 A 在运行时,可访问的段包括 LDTA 映射的 CodeA 和 DataA 段,加上 GDT 映射的操作系统的段 Codeos 和 Dataos。当任务 B 在运行时,可访问的段包括 LDTB 映射的 CodeB 和 DataB 段,加上 GDT 映射的段。
这个例子通过让每个任务使用不同的 LDT,演示了虚拟地址空间如何能够被组织成隔离每个任务。当任务 A 在运行时,任务 B 的段不是虚拟地址空间的部分,因此任务 A 没有办法访问任务 B 的内存。同样地,当任务 B 运行时,任务 A 的段也不能被寻址。这种使用 LDT 来隔离每个应用程序任务的方法,正是关键保护需求之一。
每个系统必须定义一个 GDT,并可用于系统中所有程序或任务。另外,可选定义一个或多个 LDT。例如,可以为每个运行任务定义一个 LDT,或者某些或所有任务共享一个 LDT。
GDT 本身并不是一个段,而是线性地址空间中的一个数据结构。GDT 的基线性地址和长度值必须加载进 GDTR 寄存器中。GDT 的基地址应该进行内存 8 字节对齐,以得到最佳处理器性能。GDT 的限长以字节为单位。与段类似,限长值加上基地址可得到最后表中最后一个字节的有效地址。限长为 0 表示有 1 个有效字节。因为段描述符总是 8 字节长,因此 GDT 的限长值应该设置成总是 8 的倍数减 1(即 8N-1)。
处理器并不使用 GDT 中的第 1 个描述符。把这个"空描述符"的段选择符加载进一个数据段寄存器(DS、ES、FS 或 GS)并不会产生一个异常,但是若使用这些加载了空描述符的段选择符访问内存时就肯定会产生一般保护性异常。通过使用这个段选择符初始化段寄存器,那么意外引用未使用的段寄存器肯定会产生一个异常。
LDT 表存放在 LDT 类型的系统段中。此时 GDT 必须含有 LDT 的段描述符。如果系统支持多 LDT 的话,那么每个 LDT 都必须在 GDT 中有一个段描述符和段选择符。一个 LDT 的段描述符可以存放在 GDT 表的任何地方。
访问 LDT 需使用其段选择符。为了在访问 LDT 时减少地址转换次数,LDT 的段选择符、基地址、段限长以及访问权限需要存放在 LDTR 寄存器中。
当保存 GDTR 寄存器内容时(使用 SGDT 指令),一个 48 位的"伪描述符"被存储在内存中。为了在用户模式(特权级 3)避免对齐检查出错,伪描述符应该存放在一个奇字地址处(即 地址 MOD 4 = 2)。这会让处理器先存放一个对齐的字,随后是一个对齐的双字(4 字节对齐处)。用户模式程序通常不会保存伪描述符,但是可以通过使用这种对齐方式来避免产生一个对齐检查出错的可能性。当使用 SIDT 指令保存 IDTR 寄存器内容时 也需要使用同样的对齐方式。然而,当保存 LDTR 或任务寄存器(分别使用 SLTR 或 STR 指令)时,伪描述符应该存放在双字对齐的地址处(即 地址 MOD 4 = 0)。
3、段选择符
段选择符(或称段选择子)是段的一个 16 位标识符,见图 4-10 所示。段选择符并不直接指向段,而是指向段描述符表中定义段的段描述符。段选择符 3 个字段内容:
- 请求特权级 RPL(Requested Privilege Level);
- 表指示标志 TI(Table Index);
- 索引值(Index)。
请求特权级字段 RPL 提供了段保护信息,将在后面作详细说明。表索引字段 TI 用来指出包含指定段描述符的段描述符表 GDT 或 LDT。TI=0 表示描述符在 GDT 中;TI=1 表示描述符在 LDT 中。索引字段给出了描述符在 GDT 或 LDT 表中的索引项号。可见,选择符通过定位段表中的一个描述符来指定一个段,并且描述符中包含有访问一个段的所有信息,例如段的基地址、段长度和段属性。
例如:
图 4-11(a)中选择符(0x08)指定了 GDT 中具有 RPL=0 的段 1,其索引字段值是 1,TI 位是 0,指定 GDT 表。
图 4-11(b)中选择符(0x10)指定了 GDT 中具有 RPL=0 的段 2,其索引字段值是 2,TI 位是 0,指定 GDT 表。
图 4-11(c)中选择符(0x0f)指定了 LDT 中具有 RPL=3 的段 1,其索引字段值是 1,TI 位是 1,指定 LDT 表。
图 4-11(d)中选择符(0x17)指定了 LDT 中具有 RPL=3 的段 2,其索引字段值是 2,TI 位是 1,指定 LDT 表。
实际上,图 4-11 中的前 4 个选择符:(a)、(b)、(c)和(d)分别就是 Linux 0.1x 内核的内核代码段、内核数据段、任务代码段和任务数据段的选择符。
图 4-11(e)中的选择符(0xffT)指定 LDT 表中 RPL=3 的段 8191。其索引字段值是 0b1111111111111(即 8191),TI 位等于 1,指定 LDT 表。
另外,处理器不使用 GDT 表中的第 1 项。指向 GDT 该项的选择符(即索引值为 0,TI 标志为 0 的选择符)用作为"空选择符",见图 4-11(f)所示。当把空选择符加载到一个段寄存器(除了 CS 和 SS 以外)中时,处理器并不产生异常。但是当使用含有空选择符的段寄存器用于访问内存时就会产生异常。当把空选择符加载到 CS 或 SS 段寄存器中时将会导致一个异常。
对应用程序来说段选择符是作为指针变量的一部分而可见,但选择符的值通常是由链接编辑器或链接加载程序进行设置或修改,而非应用程序。
为减少地址转换时间和编程复杂性,处理器提供可存放最多 6 个段选择符的寄存器(见图 4-12 所示),即段寄存器。每个段寄存器支持特定类型的内存引用(代码、数据或堆栈)。原则上执行每个程序都起码需要把有效的段选择符加载到代码段(CS)、数据段(DS)和堆栈段(SS)寄存器中。处理器还另外提供三个辅助的数据段寄存器(ES、FS 和 GS),可被用于让当前执行程片(或任务)能够访问其他几个数据段。
对于访问某个段的程序,必须已经把段选择符加载到一个段寄存器中。因此,尽管一个系统可以定义很多的段,但同时只有 6 个段可供立即访问。若要访问其他段就需要加载这些段的选择符。
另外,为了避免每次访问内存时都去引用描述符表,去读和解码一个段描述符,每个段寄存器都有一个"可见"部分和一个"隐藏"部分(隐藏部分也被称为"描述符缓冲"或"影子寄存器")。当一个段选择符被加载到一个段寄存器可见部分中时,处理器也同时把段选择符指向的段描述符中的段地址、段限长以及访问控制信息加载到段寄存器的隐藏部分中。缓冲在段寄存器(可见和隐藏部分)中的信息使得处理器可以在进行地址转换时不再需要花费时间从段描述符中读取基地址和限长值。
由于影子寄存器含有描述符信息的一个拷贝,因此操作系统必须确保对描述符表的改动应反映在影子寄存器中。否则描述符表中一个段的基地址或限长被修改过,但改动却没有反映到影子寄存器中。处理这种问题最简捷的方法是在对描述符表中描述符作过任何改动之后就立刻重新加载 6 个段寄存器。这将把描述符表中的相应段信息重新加载到影子寄存器中。
为加载段寄存器,提供了两类加载指令:
- 像 MOV、POP、LDS、LES、LSS、LGS 以及 LFS 指令。这些指令显式地直接引用段寄存器;
- 隐式加载指令,例如使用长指针的 CALL、JMP 和 RET 指令、IRET、INTn、INTO 和 INT3 等指令。这些指令在操作过程中会附带改变 CS 寄存器(和某些其他段寄存器)的内容。
MOV 指令当然也可以用于把段寄存器可见部分内容存储到一个通用寄存器中。
4、段描述符
前面我们已经说明了使用段选择符来定位描述符表中的一个描述符。段描述符是 GDT 和 LDT 表中的一个数据结构项,用于向处理器提供有关一个段的位置和大小信息以及访问控制的状态信息。每个段描述符长度是 8 字节,含有三个主要字段:段基地址、段限长和段属性。段描述符通常由编译器、链接器、加载器或者操作系统来创建,但绝不是应用程序。图 4-13 示出了所有类型段描述符的一般格式。
一个段描述符中各字段和标志的含义如下:
- 段限长字段 LIMIT(Segment limit field)
段限长 Limit 字段用于指定段的长度。处理器会把段描述符中两个段限长字段组合成一个 20 位的值,并根据颖粒度标志 G 来指定段限长 Limit 值的实际含义。如果 G=0,则段长度 Limit 范围可从 1 字节到 1MB 字节,单位是字节。如果 G=1,则段长度 Limit 范围可从 4KB 到 4GB,单位是4KB。
根据段类型中的段扩展方向标志 E,处理器以两种不同方式使用段限长 Limit。对于向上扩展的段。(简称上扩段),逻辑地址中的偏移值范围可以从 0 到段限长值 Limit。大于段限长 Limit 的偏移。值将产生一般保护性异常。对于向下扩展的段(简称下扩段),段限长 Limit 的含义相反。根据默认栈指针大小标志 B 的设置,偏移值范围可从段限长 Limit 到 0xFFFFFFFF 或 0xFFFF。而小于段限长 Limit 的偏移值将产生一般保护性非常。对于下扩段,减小段限长字段中的值会在该段地址空间底部分配新的内存,而不是在顶部分配。80X86 的栈总是向下扩展的,因此这种实现方式很适合扩展堆栈。
基地址字段 BASE(Base address field)
该字段定义在 4GB 线性地址空间中一个段字节 0 所处的位置。处理器会把 3 个分立的基地址字段组合形成一个 32 位的值。段基地址应该对齐 16 字节边界。虽然这不是要求的,但通过把程序的代码和数据段对齐在 16 字节边界上,可以让程序具有最佳性能。
段类型字段 TYPE(Type field)
类型字段指定段或门(Gate)的类型、说明段的访问种类以及段的扩展方向。该字段的解释依赖于描述符类型标志 S 指明是一个应用(代码或数据)描述符还是一个系统描述符。TYPE 字段的
编码对代码、数据或系统描述符都不同,见图 4-14 所示。
描述符类型标志 S(Descriptor type flag)
描述符类型标志 S 指明一个段描述符是系统段描述符(当S=0)还是代码或数据段描述符(当 S=1)。
描述符特权级字段 DPL(Descriptor privilege level)
DPL 字段指明描述符的特权级。特权级范围从 0 到 3。0 级特权级最高,3 级最低。DPL 用于控制对段的访问。
段存在标志 P(Segment present)
段存在标志 P 指出一个段是在内存中(P=1)还是不在内存中(P=0)。当一个段描述符的 P 标志为 0 时,那么把指向这个段描述符的选择符加载进段寄存器将导致产生一个段不存在异常。内存管理软件可以使用这个标志来控制在某一给定时间实际需要把那个段加载进内存中。这个功能为虚拟存储提供了除分页机制以外的控制。图 4-15 给出了当 P=0 时的段描述符格式。当 P 标志为 0时,操作系统可以自由使用格式中标注为可用(Avaliable)的字段位置来保存自己的数据,例如有关不存在段实际在什么地方的信息。
D/B(默认操作大小/默认栈指针大小和/或上界限)标志(Default operation size/default stack pointer size and/or upper bound)
根据段描述符描述的是一个可执行代码段、下扩数据段还是一个堆栈段,这个标志具有不同的功能。(对于 32 位代码和数据段,这个标志应该总是设置为 1;对于 16 位代码和数据段,这个标志被设置为 0。)
可执行代码段。此时这个标志称为 D 标志并用于指出该段中的指令引用有效地址和操作数的默认长度。如果该标志置位,则默认值是 32 位地址和 32 位或 8 位的操作数;如果该标志为 0,则默认值是 16 位地址和 16 位或 8 位的操作数。指令前缀 Cx66 可以用来选择非默认值的操作数大小;前缀 0x67 可用来选择非默认值的地址大小。
栈段(由 SS 寄存器指向的数据段)。此时该标志称为 B(Big)标志,用于指明隐含堆栈操作(例如 PUSH、POP 或 CALL)时的栈指针大小。如果该标志置位,则使用 32 位栈指针并存放在 ESP 寄存器中;如果该标志为 0,则使用 16 位栈指针并存放在 SP 寄存器中。如果堆段被设置成一个下扩数据段,这个 B 标志也同时指定了堆栈段的上界限。
下扩数据段。此时该标志称为 B 标志,用于指明堆栈段的上界限。如果设置了该标志,则堆段的上界限是 0xFFFFFFFF(4GB);如果没有设置该标志,则堆栈段的上界限是 0xFFFF(64KB)。
颗粒度标志 G(Granularity)
该字段用于确定段限长字段 Limit 值的单位。如果颗粒度标志为 0,则段限长值的单位是字节;如果设置了颗粒度标志,则段限长值使用 4KB 单位。(这个标志不影响段基地址的颗粒度,基地址的颗粒度总是字节单位。)若设置了 G 标志,那么当使用段限长来检查偏移值时,并不会去检查偏移值的 12 位最低有效位。例如,当 G=1 时,段限长为 0 表明有效偏移值为 0 到 4095。
- 可用和保留比特位(Available and reserved bits)
段描述符第 2 个双字的位 20 可供系统软件使用;位 21 是保留位并应该总是设置为 0。
5、代码和数据段描述符类型
当段描述符中 S(描述符类型)标志被置位,则该描述符用于代码或数据段。此时类型字段中最高比特位(第 2 个双字的位 11)用于确定是数据段的描述符(复位)还是代码段的描述符(置位)。
对于数据段的描述符,类型字段的低 3 位(位 8、9、10)被分别用于表示已访问 A(Accessed)、可写 W(Write-enable)和扩展方向 E(Expansion-direction),参见表 4-3 中有关代码和数据段类型字段比特位的说明。根据可写比特位 W 的设置,一个数据段可以是只读的,也可以是可读可写的。
堆栈段必须是可读/写的数据段。若使用不可写数据段的选择符加载到 SS 寄存器中,将导致一个一般保护异常。如果堆栈段的长度需要动态地改变,那么堆栈段可以是一个向下扩展的数据段(扩展方向标志置位)。这里,动态改变段限长将导致栈空间被添加到栈底部。
已访问比特位指明自从上次操作系统复位该位之后一个段是否被访问过。每当处理器把一个段的段选择符加载进段寄存器,它就会设置该位。该位需要明确地清除,否则一直保持置位状态。该位可用于虚拟内存管理和调试。
对于代码段,类型字段的低 3 位被解释成已访问 A(Accessed)、可读 R(Read-enable)和一致的 C(Conforming)。根据可读 R 标志的设置,代码段可以是只能执行、可执行/可读。当常数或其他静态数据以及指令码被放在了一个 ROM 中时就可以使用一个可执行/可读代码段。这里,通过使用带 CS 前缀的指令或者把代码段选择符加载进一个数据段寄存器(DS、ES、FS 或 GS),我们可以读取代码段中的数据。在保护模式下,代码段是不可写的。
代码段可以是一致性的或非一致性的。向更高特权级一致性代码段的执行控制转移,允许程序以当前特权级继续执行。向一个不同特权级的非一致性代码段的转移将导致一般保护异常,除非使用了一个调用门或任务门(有关一致性和非一致性代码段的详细信息请参见"直接调用或跳转到代码段")。不访问保护设施的系统工具以及某些异常类型(例如除出错、溢出)的处理过程可以存放在一致性代码段中。需要防止低特权级程序或过程访问的工具应该存放在非一致性代码段中。
所有数据段都是非一致性的,即意味着它们不能被低特权级的程序或过程访问。然而,与代码段不同,数据段可以被更高特权级的程序或过程访问,而无须使用特殊的访问门。
如果 GDT 或 LDT 中一个段描述符被存放在 ROM 中,那么若软件或处理器试图更新(写)在 ROM中的段描述符时,处理器就会进入一个无限循环。为了防止这个问题,需要存放在 ROM 中的所有描述符的已访问位应该预先设置成置位状态。同时,删除操作系统中任何试图修改 ROM 中段描述符的代码。
6、系统描述符类型
当段描述符中的 S 标志(描述符类型)是复位状态(0)的话,那么该描述符是一个 系统描述符。处理器能够识别以下一些类型的系统段描述符:
- 局部描述符表(LDT)的段描述符;
- 任务状态段(TSS)描述符;
- 调用门描述符;
- 中断门描述符;
- 陷阱门描述符;
- 任务门描述符。
这些描述符类型可分为两大类:系统段描述符 和 门描述符 。系统段描述符 指向系统段(如 LDT 和 TSS 段),门描述符 就是一个"门",对于调用、中断或陷阱门,其中含有代码段的选择符和段中程序入口点的指针;对于任务门,其中含有 TSS 的段选择符。表 4-4 给出了系统段描述符和门描述符类型字段的编码。
有关 TSS 状态段和任务门的使用方法将在任务管理一节中进行说明,调用门的使用方法将放在保护一节中说明,中断和陷阱门的使用方法将在中断和异常处理一节中给予说明。
Linux0.11 80X86知识(七)(下):https://developer.aliyun.com/article/1597284?spm=a2c6h.13148508.setting.16.19d14f0etEXyxx