分段机制(个人理解)
Posted TTC
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分段机制(个人理解)相关的知识,希望对你有一定的参考价值。
分段机制可用于实现多种系统设计。这些设计范围从使用分段机制的最小功能来保护程序的平坦模型,到使用分段机制创建一个可同时可靠地运行多个程序(或任务)的具有稳固操作环境的多段模型。
多段模型能够利用分段机制全部功能提供由硬件增强的代码,数据结构,程序和任务的保护措施。通常,每个程序(或任务)都是用自己的段描述符以及自己的段。对程序来说段能够完全是私有的,或者是程序之间共享的。对所有段以及系统上运行程序各自执行环境的访问都由硬件控制。
访问检查不仅能够用来保护对段界限以外地址的引用,而且也能用来在某些段中防止执行不允许的操作。例如,因为代码段被设计成是只读形式的段,因此可以用硬件来防止对代码段执行写操作。段中的访问权限信息也可以用来设置保护环或级别。保护级别可用于保护操作系统程序不受应用程序非法访问。
- 段的定义 : 保护模式中的80x86 提供了4GB 的物理地址空间。这是处理器在其地址总线上可以寻址的地址空间。这个地址空间是平坦的,地址范围从0 到 0xFFFFffff .这个物理地址空间可以映射到读写内存,只读内存以及内存映射I/O中。 分段机制就是把 虚拟地址空间中的虚拟内存组织成一些长度可变的称为段的内存块单元。80386 虚拟地址空间中的虚拟地址(逻辑地址)由一个段部分和一个偏移部分构成。段是虚拟地址到线性地址转换机制的基础。每个段由三个参数定义:
- 段基地址(Base address),指定段在线性地址空间中的开始地址。基地址是线性地址,对应于段中偏移0 处。
- 段限长(limit),是虚拟地址空间中段内最大可用偏移位置。它定义了段的长度。
- 段属性(Attributes),指定段的特性。例如该段是否可读,可写或可作为一个程序执行:段的特权级等。
- 段限长定义了在虚拟地址空间中段的大小。段基址和段限长定义了段所映射的线性地址范围或区域。 段内0 到 limit 的地址范围对应线性地址中年范围 base 到 base + limit 。偏移量大于段限长的虚拟地址是无意义的,如果使用则会导致异常。另外 ,若访问一个段并没有得到段属性许可则也会导致异常。例如,如果你试图写一个只读的段,那么80386 就会产生一个异常。 另外,多个段映射到线性地址中的范围可以部分重叠或者覆盖,甚至完全重叠,如下图所示(一个任务的Code Segment 和 Data Segment 的段限长相同,并被映射到线性地址完全相同而重叠的区域上)。
- 段的基地址,段限长以及段的保护属性存储在一个称为段描述符(Segment Descriptor) 的结构项中。在逻辑地址到线性地址的转换映射过程中会使用这个段描述符。段描述符保存在内存中的段描述符表中(Descriptor table)。段描述符表是包含段描述符项的一个简单数组。 前面介绍的段选择符即用于通过指定表中一个段描述符的位置来指定相应的段。
- 即使利用段的最凶功能,使用逻辑地址也能访问处理器地址空间中的每个字节。逻辑地址由16 bit 的段选择符和 32 bit 的偏移量组成,如下图 4-7 所示。 段选择符指定字节所在的段,而偏移量指定该字节在段中相对于段基地址的位置。处理器会把每个逻辑地址转换成线性地址。线性地址是处理器线性地址空间中的32 bit 地址。 与物理地址空间类似,线性地址空间也是平坦的4GB 地址空间,地址范围从 0 到 0xFFFFffff。线性地址空间中含有为系统定义的所有段和系统表。 为了把逻辑地址转换为一个 线性地址,处理器会执行以下操作:
- 使用段选择符中的偏移值(段索引)在GDT 或LDT 表中定位相应的段描述符。(仅当一个新的段选择符加载到段寄存器中时才需要这一步)。
- 利用段描述符检验段的访问权限和范围,以确保该段是可访问的并且偏移量位于段界限内。
- 把段描述符中取得的段基地址加到偏移量上,最后形成一个线性地址。
- 如果没有开启分页,那么处理器直接把线性地址映射到物理地址(即线性地址被送到处理器地址总线上)。如果对线性地址空间进行了分页处理,那么就会使用二级地址转换把线性地址转换成物理地址。
- 段描述符表
- 段描述符表是段描述符的一个数组,如下图所示。描述符表的长度可变,最多可以包含8192个 8 byte 描述符。有两个描述符表: 全局描述符表GDT (Global descriptor table); 局部描述符表 LDT (Local descriptor table)。
- 描述符表存储在由操作系统维护着的特殊数据结构中,并且由处理器的内存管理硬件来引用。这些特殊结构应该保存在仅仅由操作系统软件访问的受保护的内存区域中,以防止应用程序修改其中的地址转换信息。 虚拟地址空间被分割成大小相等的两半。 一半由GDT 来映射变换到线性地址,另一半则由 LDT 来映射。整个虚拟地址空间共含有2^14个段: 一半空间(即2^13 个段) 是由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 和Data a 段 ,加上 GDT 映射的操作系统的段 Codeos 和 Dataos。 当任务B 在运行时,可访问的段包括LDTb 映射的Code b 和Datab段,加上GDT 映射的段 Codeos 和 Dataos。
- 这个例子通过让每个任务使用不同的LDT,演示了虚拟地址空间如何能够被组织成隔离每个任务。当任务A在运行时,任务B的段不是虚拟地址空间的部分,因此任务A没有方法访问任务B的内存。同样的,当任务B运行时,任务A的段也不能被寻址。这种使用LDT 来隔离每个应用程序任务的方法,正是关键保护需求之一。
- 每个系统必须定义一个GDT,并可用于系统中所有程序或者任务。另外,可选定义一个或多个LDT。例如,可以为每个运行任务定义一个LDT,或者某些或所有任务共享一个LDT。
- GDT 本身并不是一个段,而是线性地址空间中的一个数据结构。GDT 的基线性地址和长度值必须加载进GDTR 寄存器中。GDT 的基地址应该进行内存 8bit 对齐,以得到最佳处理器性能。GDT 的限长以字节为单位。 与段类似,限长值加上基地址可得到最后表中最后一个字节的有效地址。限长为 0 表示有 1个有效字节。 因为段描述符总是 8byte 长,因此GDT 的限长值 应该设置成总是 8 的倍数 减1 (即 8N -1);
- 处理器并不使用GDT 中的第一个描述符。把这个“空描述符”的段选择符加载进一个数据段寄存器(DS,ES,FS,GS)并不会产生一个异常,但是若使用这些加载了空描述符的段选择符访问内存时就肯定会产生一般保护性异常。通过使用这个段选择符初始化段寄存器,那么意外引用未使用的段寄存器肯定会产生一个异常。
- LDT 表存放在LDT 类型的系统段中,此时GDT 必须含有LDT 的段描述符。如果系统支持多LDT的话,那么每个LDT都必须在GDT中有一个段描述符和段选择符。一个LDT 的段描述符可以存放在GDT表的任何地方。
- 访问LDT 需使用其段选择符。为了在访问LDT时减少地址转换次数,LDT的段选择符,基地址,段限长以及访问权限需要存放在LDTR寄存器中。
- 当保存GDTR 寄存器内容时(使用SGDT指令),一个48bit 的“伪描述符”被存储在内存中。为了在用户模式(R3)避免对齐检查出错,伪描述符应该存放在一个奇字地址处(即 地址 MOD 4 =2)。 这会让处理器先存放一个对齐的字,随后是一个对齐的双字(4 byte 对齐处)。用户模式程序通常不会保存伪描述符,但是可以通过使用这种对齐方式来避免产生一个对齐检查出错的可能性。当使用SIDT指令保存IDTR寄存器内容时也需要使用同样的对齐方式。然而,当保存LDTR 或任务寄存器(分别使用SLTR 或 STR 指令)时,伪描述符应该存放在双字对齐的地址处(即 地址 MOD 4 = 0)。
- 段选择符
- 段选择符(或称 段选择子) 是段的一个16 bit 标识符,如下图。 段选择符并不直接指向段,而是指向段描述符表中定义段的段描述符。段选择符3个字段内容:
- 请求特权级 RPL(Request Privilege Level);
- 表指示标志TI (Table Index);
- 索引值(Index)。
- 请求特权级字段RPL 提供了段保护信息,将在后面作详细说明。表索引字段TI 用来指出包含指定段描述符的段描述符表GDT 或 LDT 。TI = 0表示描述符在GDT 中; TI =1 表示 描述符在LDT 中。 索引字段给出了描述符在GDT 或LDT 表中的索引项号。可见,选择符通过定位段表中的一个描述符来指定一个段,并且描述符中包含有访问一个段的所有信息,例如段的基地址,段长度和段属性。
- 例如下图4-11(a)中选择符(0x80)指定了GDT 中具有RPL =0 的段1, 其索引字段是1,TI bit 是 0, 指定GDT 表。 图4-11(b)中选择符(0x10)指定了GDT 中具有 RPL =0 的段2,其索引字段值是2, TI bit 是0, 指定GDT 表。 图 4-11(c)中选择符(0x0f)指定了LDT 中具有RPL =3 的段1,其索引字段值是1, TI bit 是1,指定LDT 表。 图 4-11(d)中选择符(0x17)指定了LDT 中具有RPL=3 的段2,其索引字段值是 2 ,TI bit 是1,指定LDT 表。 实际上图4-11 中的前4个选择符 (a)(b)(c)(d) 分别就是Linux 0.1x kernel 的内核代码段,内核数据段,任务代码段 和 任务数据段的选择符。 图4-11 (e)的选择符(0xffff)指定LDT 表中RPL =3 的段8191.其索引字段值是0b111111111111(即使8191),TI bit 为1,指定LDT 表。
- 另外,处理器不使用GDT 表中的第一项。指向GDT 该项的选择符(即索引值为0,TI 标志位0的选择符)用作为“空选择符”,见图4-11(f)所示。当把空选择符加载到一个段寄存器(除了CS 和SS 以外)中时,处理器并不产生异常。但是当使用含有空选择符的段寄存器用于访问内存时就会产生异常。当把空选择符加载到CS 或 SS 段寄存器中时将会导致一个异常。
- 对应用程序来说段选择符是作为指针变量的一部分而可见,但选择符的值通常是由链接编辑器或链接加载程序进行设置或修改,而非应用程序。
- 为减少地址转换时间和编程复杂性,处理器提供可存放最多6个段选择符的寄存器(见图4-12所示),即段寄存器。每个段寄存器支持特定类型的内存引用(代码,数据,或堆栈)。原则上执行每个程序都起码需要把有效的段选择符加载到代码段(CS),数据段(DS),堆栈段(SS)寄存器中。处理器还另外提供段。
- 对于访问某个段的程序,必须已经把段选择符加载到一个段寄存器中。因此,尽管一个系统可以定义很多的段,但同时只有6个段可供立即访问。若要访问其他段就需要加载这些段的选择符。
- 另外,为了避免每次访问内存时都去引用描述符表,去读和解码一个段描述符,每个段寄存器都有一个“可见”部分和一个“隐藏”部分(隐藏部分也被称为“描述符缓冲” 或 “影子寄存器”)。当一个段选择符被加载到一个段寄存器可见部分中时,处理器也同时把段选择符指向的段描述符中的段地址,段限长以及访问控制信息加载到段寄存器的隐藏部分中。缓冲在段寄存器(可见和隐藏部分)中的信息使得处理器可以在进行地址转换时不再需要花费时间从段描述符中读取基地址和限长值。
- 由于影子寄存器含有描述符信息的一个copy,因此操作系统必须确保对描述符表的改动应反映在影子寄存器中。否则描述符表中一个段的基地址或限长被修改过,但改动却没有反映到影子寄存器中。处理这种问题最简洁的方法是在对描述符表中描述符作过任何改动之后就立刻加载6个段寄存器。这将把描述符表的相应信息重新加载到影子寄存器中。
- 为加载段寄存器,提供了两类加载指令: MOV , POP,LDS, LES,LSS,LGS 以及LFS 指令。这些指令显式地直接引用段寄存器;
- 隐式加载指令,例如使用长指针的CALL , JMP 和RET 指令, IRET ,INTn ,INTO 和INT3 等指令。这些指令在操作过程中会附带改变CS 寄存器(和某些其他段寄存器)的内容。
- 段选择符(或称 段选择子) 是段的一个16 bit 标识符,如下图。 段选择符并不直接指向段,而是指向段描述符表中定义段的段描述符。段选择符3个字段内容:
- 段描述符 : 前面已经说明了使用段选择符来定位描述符表中的一个描述符。段描述符是GDT 和LDT 表中的一个数据结构项,用于向处理器提供有关一个段的位置和大小信息以及访问控制的状态信息。每个段描述符长度 是 8byte ,含有三个主要字段: 段基地址,段限长和段属性。 段描述符通常由编译器,链接器,加载器或者操作系统来创建,但绝不是应用程序。 图4-13 所示了所有类型段描述符的一般格式。
- 一个段描述符中各字段和标志的含义如下:
- 段限长字段LIMIT (Segment limit field) 段限长Limit 字段用于指定段的长度。处理器会把段描述符中两个段限长字段组合成一个20 bit的值,并根据颗粒度标志G 来指定段限长Limit 值得实际含义。 如果G=0,则段长度Limit 范围可从 1byte 到 1MB 字节,单位是字节。 如果 G=1,则段长度Limit 范围可从4KB 到 4GB,单位是 4kb。
- 基地址字段BASE (Base address field) : 该字段定义在4GB 线性地址空间中一个段字节 0 所处的位置。处理器会把3个分立的基地址字段组合形成一个 32 bit 的值。段基地址应该对齐16 byte 边界。虽然这不是要求的,但通过把程序的代码和数据段对齐在16 byte 边界上,可以让程序具有最佳性能。
- 段类型字段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 bit 代码和数据段,这个标志应该总是设置为1 ; 对于16 bit 代码和数据段,这个标志被设置为0)。
- 颗粒度标志G(Granularity):该字段用于确定段限长字段Limit 值得单位。如果颗粒度标志位0,则段限长值得单位是字节;如果设置了颗粒度标志,则段限长值使用4kb 单位。(这个标志不影响段基地址的颗粒度,基地址的颗粒度总是字节单位)。若设置了G 标志,那么当使用段限长来检查偏移值时,并不会去检查偏移值的12 bit 最低有效位。例如,当 G=1时,段限长为0 表明有效偏移值为 0 到 4095.
- 可用和保留比特位(Available 按到 reserved bits): 段描述符第2个双字的位 20 可供系统软件使用; 位 21 是保留位并应该总是设置为0.
- 一个段描述符中各字段和标志的含义如下:
- 代码和数据段描述符类型:
- 当段描述符中S (描述符类型)标志被置位,则该描述符用于代码或数据段。此时类型字段中最高比特位(第2个双字的 位11)用于确定是数据段的描述符(复位)还是代码段的描述符(置位)。
- 对于数据段的描述符,类型字段的低3 位(位 8,9,10)被分别用于表示已访问A (Accessed),可写W (Write-enable)和扩展方向E (Expansion-direction),参见表4-3 中有关代码和数据段类型比特位的说明。根据可写比特位W的设置,一个数据段可以是只读的,也可以是可读可写的。
- 堆栈段必须是可读/可写的数据段。若使用不可写数据段的选择符加载到SS寄存器中,将导致一个一般保护异常。如果堆栈段的长度需要动态地改变,那么堆栈段可以是一个向下扩展的数据段(扩展方向标志置位)。这里,动态改变段限长将导致栈空间被添加到栈底部。
- 已访问比特位指明自从上次操作系统复位该位之后一个段是否被访问过。每当处理器把一个daunting的段选择符加载进段寄存器,它就会设置该位。该位需要明确地清除,否则一直保持置位状态。该位可用于虚拟内存管理和调试。
- 对于代码段,类型字段的低3 位被解释成已访问A(Accessed) ,可读R(Read-enable) 和一致的C(Conforming)。根据可读R标志的设置,代码段可以是只能执行,可执行/可读。当常数或其他静态数据以及指令码被放在一个ROM 中时就可以使用一个可执行、可读代码段。这里,通过使用带CS前缀的指令或者把代码段选择符加载进一个数据段寄存器(DS,ES,FS,GS),我们可以读取代码段中的数据。在保护模式下,代码段是不可写的。
- 代码段可以是一致性的或非一致性的。向更高特权级一致性代码段的执行控制转移,允许程序以当前特权级继续执行。想一个不同特权级的非一致性代码段的转移将导致一般保护异常,除非使用了一个调用门或任务门(有关一致性和非一致性代码段的详细信息请参考其他资料)。不访问保护设施的系统工具以及某些异常类型(例如出错,溢出)的处理过程可以存放在一致性代码段中。需要防止低特权级程序或过程访问的工具应该存放在非一致性代码段中。
- 所有数据段都是非一致性的,即意味着它们不能被低特权级的程序或过程访问。然而,与代码段不同,数据段可以被更高特权级的程序或过程访问,而无须使用特殊的访问门。
- 如果GDT 或LDT 中一个段描述符被存放在ROM 中,那么若软件或处理器试图更新(写)在ROM 中的段描述符时,处理器就会进入一个无限循环。同时,删除操作系统中任何试图修改ROM中段描述符的代码。
- 系统描述符类型:
- 当段描述符中的S 标志(描述符类型)是复位状态(0)的话,那么该描述符是一个系统描述符。处理器能够识别以下一些类型的系统段描述符:
- 局部描述符表(LDT)的段描述符
- 任务状态段(TSS)描述符
- 调用门描述符
- 中断门描述符
- 陷阱门描述符
- 任务门描述符
- 这些描述符类型可分为两大类:系统段描述符和门描述符。系统段描述符指向系统段(如LDT 和TSS段).门描述符就是一个“门”,对于调用、中断、陷阱门,其中含有代码段的段选择子和处理函数的入口地址(为相应的idt数组内容);对于任务门,其中含有TSS 的段选择符。表4-4 给出了系统段描述符和门描述符类型字段的编码。
- 有关TSS状态段和任务门的使用方法将在任务管理中说明,调用门的使用方法将放在保护中说明;中断和陷阱门的使用方法在中断和异常处理中说明。
以上是关于分段机制(个人理解)的主要内容,如果未能解决你的问题,请参考以下文章