Skip to content

Latest commit

 

History

History
470 lines (322 loc) · 32.5 KB

Segment.md

File metadata and controls

470 lines (322 loc) · 32.5 KB

80x86保护模式编程

内存管理寄存器

处理器提供了4个内存管理寄存器: GDTR, LDTR, IDTR, TR. 用于指定内存分段管理所用系统表的基地址,处理器为这些寄存器的加载和保存提供了特定的指令. 如下图所示:

https://github.com/novelinux/arch-x86/blob/master/res/gdtr_idtr_tr_ldtr.jpg

全局描述符表寄存器GDTR

GDTR寄存器: 用于存放全局描述符表GDT的32位的线性基地址和16位的表限长值。 基地址指定GDT表中字节0在线性地址空间中的地址,表长度指明GDT表的字节长度值。

指令LGDT和SGDT分别用于加载和保存GDTR寄存器的内容。

在机器刚加电或处理器复位后,基地址被默认地设置为0,而长度值被设置成0xFFFF。 在保护模式初始化过程中必须给GDTR加载一个新值。

中断描述符表寄存器IDTR

IDTR寄存器: 用于存放中断描述符表IDT的32位线性基地址和16位表长度值。 指令LIDT和SIDT分别用于加载和保存IDTR寄存器的内容。

在机器刚加电或处理器复位后,基地址被默认地设置为0,而长度值被设置成0xFFFF。

局部描述符表寄存器LDTR

LDTR寄存器: 用于存放局部描述符表LDT的32位线性基地址、16位段限长和描述符属性值。

指令LLDT和SLDT分别用于加载和保存LDTR寄存器的段描述符部分。 包含LDT表的段必须在GDT表中有一个段描述符项。

  • 当使用LLDT指令把含有LDT表段的选择符加载进LDTR时,LDT段描述符的段基地址、段限长度以及描述符属性 会被自动地加载到LDTR中。

  • 当进行任务切换时,处理器会把新任务LDT的段选择符和段描述符自动地加载进LDTR中。

在机器加电或处理器复位后,段选择符和基地址被默认地设置为0,而段长度被设置成0xFFFF。

任务寄存器TR

TR寄存器: 用于存放当前任务TSS段的16位段选择符、32位基地址、16位段长度和描述符属性值。 它引用GDT表中的一个TSS类型的描述符, 指令LTR和STR分别用于加载和保存TR寄存器的段选择符部分。

  • 当使用LTR指令把选择符加载进任务寄存器时,TSS描述符中的段基地址、段限长度以及描述符属性会被自动 加载到任务寄存器中。

  • 当执行任务切换时,处理器会把新任务的TSS的段选择符和段描述符自动加载进任务寄存器TR中。

控制寄存器

控制寄存器(CR0~CR3)用于控制和确定处理器的操作模式以及当前执行任务的特性. 如下图所示:

https://github.com/novelinux/arch-x86/blob/master/res/cr.jpg

  • CR0中含有控制处理器操作模式和状态的系统控制标志;
  • CR1保留不用;
  • CR2含有导致页错误的线性地址;
  • CR3中含有页目录表物理内存基地址,因此该寄存器也被称为页目录基地址寄存器PDBR.

CR0中保护控制位

  • PE: CR0的位0是启用保护(Protection Enable)标志。 当设置该位时即开启了保护模式;当复位时即进入实地址模式。这个标志仅开启段级保护,而并没有启用 分页机制。若要启用分页机制,那么PE和PG标志都要置位。

  • PG: CR0的位31是分页(Paging)标志。 当设置该位时即开启了分页机制;当复位时则禁止分页机制,此时所有线性地址等同于物理地址。 在开启这个标志之前必须已经或者同时开启PE标志。即若要启用分页机制,那么PE和PG标志都要置位。

  • WP:对于Intel 80486或以上的CPU,CR0的位16是写保护(Write Proctect)标志。 当设置该标志时,处理器会禁止超级用户程序(例如特权级0的程序)向用户级只读页面执行写操作; 当该位复位时则反之。该标志有利于UNIX类操作系统在创建进程时实现写时复制(Copy on Write)技术。

启用保护模式PE(Protected Enable)位(位0)和开启分页PG(Paging)位(位31)分别用于控制分段和分页机制。

PE用于控制分段机制:

  • 如果PE=1,处理器就工作在开启分段机制环境下,即运行在保护模式下。
  • 如果PE=0,则处理器关闭了分段机制,并如同8086工作于实地址模式下。

PG用于控制分页机制:

  • 如果PG=1,则开启了分页机制。

  • 如果PG=0,分页机制被禁止,此时线性地址被直接作为物理地址使用。

  • 如果PE=0、PG=0,处理器工作在实地址模式下;

  • 如果PG=0、PE=1,处理器工作在没有开启分页机制的保护模式下;

  • 如果PG=1、PE=0,此时由于不在保护模式下不能启用分页机制,因此处理器会产生一个一般保护异常,即这种标志组合无效;

  • 如果PG=1、PE=1,则处理器工作在开启了分页机制的保护模式下。

注意: 当改变PE和PG位时,必须小心。只有当执行程序至少有部分代码和数据在线性地址空间和物理地址 空间中具有相同地址时,我们才能改变PG位的设置。此时这部分具有相同地址的代码在分页和未分页世界之间 起着桥梁的作用。无论是否开启分页机制,这部分代码都具有相同的地址。另外,在开启分页(PG=1)之前 必须先刷新页高速缓冲TLB。

在修改该了PE位之后程序必须立刻使用一条跳转指令,以刷新处理器执行管道中已经获取的不同模式下的任何 指令。在设置PE位之前,程序必须初始化几个系统段和控制寄存器。在系统刚上电时,处理器被复位成PE=0和 PG=0(即实模式状态),以允许引导代码在启用分段和分页机制之前能够初始化这些寄存器和数据结构。

CR3

  • CR3含有存放页目录表页面的物理地址,因此CR3也被称为PDBR。 因为页目录表页面是页对齐的, 所以该寄存器只有高20位是有效的。 而低12位保留供更高级处理器使用,因此在往CR3中加载一个新值时低12位必须设置为0。

使用MOV指令加载CR3时具有让页高速缓冲无效的副作用。为了减少地址转换所要求的总线周期数量, 最近访问的页目录和页表会被存放在处理器的页高速缓冲器件中,该缓冲器件被称为转换查找缓冲区 (Translation Lookaside Buffer,TLB)。只有当TLB中不包含要求的页表项时才会使用额外的总线周期从 内存中读取页表项。

即使CR0中的PG位处于复位状态(PG=0),我们也能先加载CR3。以允许对分页机制进行初始化。 当切换任务时,CR3的内容也会随之改变。但是如果新任务的CR3值与原任务的一样,处理器就无需刷新页 高速缓冲。这样共享页表的任务可以执行得更快。

CR2

CR2用于出现页异常时报告出错信息。在报告页异常时,处理器会把引起异常的线性地址存放在CR2中。 因此操作系统中的页异常处理程序可以通过检查CR2的内容来确定线性地址空间中哪一个页面引发了异常。

地址变换

任何完整的内存管理系统都包含两个关键部分:保护和地址变换。

  • 提供保护措施可以防止一个任务访问另一个任务或操作系统的内存区域。
  • 地址变换能够让操作系统在给任务分配内存时具有灵活性,并且因为我们可以让某些物理地址不被任何逻辑地址所映射,所以在地址变换过程中同时也提供了内存保护功能。

正如上面提到的,计算机中的物理内存是字节的线性数组,每字节具有一个唯一的物理地址; 程序中的地址是由两部分构成的逻辑地址。这种逻辑地址并不能直接用于访问物理内存,而需要使用地址变换 机制将它变换或映射到物理内存地址上。内存管理机制即用于将这种逻辑地址转换成物理内存地址。

为了减少确定地址变换所需要的信息,变换或映射通常以内存块作为操作单位。分段机制和分页机制是两种 广泛使用的地址变换技术。它们的不同之处在于逻辑地址如何组织成被映射的内存块、变换信息如何指定以及 编程人员如何进行操作。分段和分页操作都使用驻留在内存中的表来指定它们各自的变换信息。这些表只能由 操作系统访问,以防止应用程序擅自修改。

80x86在从逻辑地址到物理地址变换过程中使用了分段和分页两种机制. 如下图所示:

https://github.com/novelinux/arch-x86/blob/master/res/address_convert.jpg

  • 第一阶段使用分段机制把程序的逻辑地址变换成处理器可寻址内存空间(称为线性地址空间)中的地址。
  • 第二阶段使用分页机制把线性地址转换为物理地址。

注意: 在地址变换过程中,第一阶段的分段变换机制总是使用的,而第二阶段的分页机制则是供选用的。 如果没有启用分页机制,那么分段机制产生的线性地址空间就直接映射到处理器的物理地址空间上。物理地址 空间定义为处理器在其地址总线上能够产生的地址范围。

分段机制

分段提供了隔绝各个代码、数据和堆栈区域的机制,因此多个程序(或任务)可以运行在同一个处理器上而不会互相干扰。 分页机制为传统需求页、虚拟内存系统提供了实现机制。其中虚拟内存系统用于实现程序代码按要求被映射到物理内存中。分页机制当然也能用于提供多任务之间的隔离措施。

如下图所示:

https://github.com/novelinux/arch-x86/blob/master/res/segment.jpg

分段提供了一种机制,用于把处理器可寻址的线性地址空间划分成一些较小的称为段的受保护地址空间区域。 段可以用来存放程序的代码、数据和堆栈,或者用来存放系统数据结构(如TSS或LDT)。如果处理器中有多个 程序或任务在运行,那么每个程序可分配各自的一套段。此时处理器就可以加强这些段之间的界限,并且确保 一个程序不会通过访问另一个程序的段而干扰程序的执行。分段机制还允许对段进行分类。这样,对特定类型 段的操作能够受到限制。

注意: 一个系统中所有使用的段都包含在处理器线性地址空间中。

为了定位指定段中的一个字节,程序必须提供一个逻辑地址。

逻辑地址包括: 一个段选择符和一个偏移量。段选择符是一个段的唯一标识. 另外,段选择符提供了 段描述符表(如全局描述符表GDT)中一个数据结构(称为段描述符)的偏移量。

每个段都有一个段描述符, 段描述符指明段的大小、访问权限和段的特权级、段类型以及段的第1字节在 线性地址空间中的位置(称为段的基地址)。逻辑地址的偏移量部分加到段的基地址上就可以定位段中某 字节的位置。因此基地址加上偏移量就形成了处理器线性地址空间中的地址。

线性地址空间与物理地址空间具有相同的结构。相对于二维的逻辑地址空间来说,它们都是一维地址空间。 虚拟地址(逻辑地址)空间可包含最多16K个段,而每个段最长可达4GB(232B),使得虚拟地址空间容量 达到64TB(246)。线性地址空间和物理地址空间都是4GB。实际上,如果禁用分页机制,那么线性地址空间 就是物理地址空间。

段的定义

保护模式中80x86 提供了4GB的物理地址空间。这是处理器在其地址总线上可以寻址的地址空间。 这个地址空间是平坦的,地址范围从0到0xFFFFFFFF。这个物理地址空间可以映射到读写内存、只读内存以及 内存映射I/O中。分段机制就是把虚拟地址空间中的虚拟内存组织成一些长度可变的称为段的内存块单元。 80386虚拟地址空间中的虚拟地址(逻辑地址)由一个段部分和一个偏移部分构成。

段是虚拟地址到线性地址转换机制的基础。

每个段由以下几个参数定义:

  • 段基地址(Base Address): 指定段在线性地址空间中的开始地址。基地址是线性地址,对应于段中偏移0处。
  • 段限长(Limit): 是虚拟地址空间中段内最大可用偏移位置。它定义了段的长度。
  • 段属性(Attributes): 指定段的特性。例如该段是否可读、可写或可作为一个程序执行;段的特权级等。

段限长定义了在虚拟地址空间中段的大小。段基址和段限长定义了段所映射的线性地址范围或区域。 段内0到limit的地址范围对应线性地址中范围Base到Base+Limit。 偏移量大于段限长的虚拟地址是无意义的,如果使用则会导致异常。另外,若访问一个段并没有得到段属性 许可则也会导致异常。例如,如果你试图写一个只读的段,那么80386就会产生一个异常。另外,多个段映射 到线性地址中的范围可以部分重叠或覆盖,甚至完全重叠。

在Linux 0.1x系统中,一个任务的代码段和数据段的段限长相同,并被映射到线性地址完全相同而重叠的区域上.

段描述符表

段描述符表是段描述符的一个数组,如下图所示:

https://github.com/novelinux/arch-x86/blob/master/res/gdt_ldt.jpg

描述符表的长度可变,最多可以包含8192个8字节描述符。 有两种描述符表:全局描述符表GDT(Global Descriptor Table)和局部描述符表LDT(Local Descriptor Table).

描述符表存储在由操作系统维护着的特殊数据结构中,并且由处理器的内存管理硬件来引用。 这些特殊结构应该保存在仅由操作系统软件访问的受保护的内存区域中,以防止应用程序修改其中的地址转换 信息。

注意: 虚拟地址空间被分割成大小相等的两半。一半由GDT来映射变换到线性地址(全局虚拟地址空间), 另一半则由LDT来映射(局部虚拟地址空间)。 通过指定一个描述符表(GDT或LDT)以及表中描述符号,我们就可以定位一个描述符。

当发生任务切换时,LDT会更换成新任务的LDT,但是GDT并不会改变。因此,GDT所映射的一半虚拟地址空间是 系统中所有任务共有的,但是LDT所映射的另一半则在任务切换时被改变。系统中所有任务共享的段由GDT来 映射。这样的段通常包括含有操作系统的段以及所有任务各自的包含LDT的特殊段。LDT段可以想象成属于 操作系统的数据。

实例

下图表明一个任务中的段如何能在GDT和LDT之间分开:

https://github.com/novelinux/arch-x86/blob/master/res/example1.jpg

图中共有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的限长以字节为单位。 与段类似,限长值加上基地址可得到最后表中最后1字节的有效地址。限长为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寄存器中。

段选择符

段选择符(或称段选择子)是段的一个16位标识符,如下图所示:

https://github.com/novelinux/arch-x86/blob/master/res/selector.jpg

段选择符并不直接指向段,而是指向段描述符表中定义段的段描述符。段选择符的3个字段分别是:

  • 请求特权级RPL(Requested Privilege Level);
  • 表指示标志TI(Table Index);
  • 索引值(Index);

请求特权级字段RPL提供了段保护信息。表索引字段TI用来指出包含指定段描述符的段描述符表GDT或LDT。 TI=0表示描述符在GDT中;TI=1表示描述符在LDT中。索引字段给出了描述符在GDT或LDT表中的索引项号。 可见,选择符通过定位段表中的一个描述符来指定一个段,并且描述符中包含访问一个段的所有信息, 如段的基地址、段长度和段属性。

实例

如下图所示:

https://github.com/novelinux/arch-x86/blob/master/res/example2.jpg

  • a中选择符(0x08)指定了GDT中具有RPL=0的段1,其索引字段值是1,TI位是0,指定GDT表。
  • b中选择符(0x10)指定了GDT中具有RPL=0的段2,其索引字段值是2,TI位是0,指定GDT表。
  • c中选择符(0x0f)指定了LDT中具有RPL=3的段1,其索引字段值是1,TI位是1,指定LDT表。
  • d中选择符(0x17)指定了LDT中具有RPL=3的段2,其索引字段值是2,TI位是1,指定LDT表。

注意: 实际上,图中的前4个选择符a~d分别就是Linux 0.1x内核的内核代码段、内核数据段、任务代码段和任务数据段的选择符。

  • e中的选择符(0xffff)指定LDT表中RPL=3的段8191。其索引字段值是0b1111111111111(即8191),TI位等于1,指定LDT表。

另外,处理器不使用GDT表中的第1项。指向GDT该项的选择符(即索引值为0,TI标志为0的选择符)用作 "空选择符",如图f所示。当把空选择符加载到一个段寄存器(除了CS和SS以外)中时,处理器并不产生异常。 但是当使用含有空选择符的段寄存器用于访问内存时就会产生异常。当把空选择符加载到CS或SS段寄存器中时 将会导致一个异常。

对应用程序来说段选择符是作为指针变量的一部分而可见,但选择符的值通常是由链接编辑器或链接加载程序进行设置或修改,而非应用程序。

为减少地址转换时间和编程复杂性,处理器提供可存放最多6个段选择符的寄存器(如下图所示),即 段寄存器。每个段寄存器支持特定类型的内存引用(代码、数据或堆栈)。原则上执行每个程序都起码需要 把有效的段选择符加载到代码段(CS)、数据段(DS)和堆栈段(SS)寄存器中。处理器还另外提供3个辅助 的数据段寄存器(ES、FS和GS),以便当前执行程序(或任务)能够访问其他几个数据段。

https://github.com/novelinux/arch-x86/blob/master/res/seg_reg.jpg

对于访问某个段的程序,必须已经把段选择符加载到一个段寄存器中。因此,尽管一个系统可以定义很多的段, 但同时只有6个段可供立即访问。若要访问其他段就需要加载这些段的选择符。

另外,为了避免每次访问内存时都去引用描述符表,去读和解码一个段描述符,每个段寄存器都有一个"可见" 部分和一个"隐藏"部分(隐藏部分也被称为"描述符缓冲"或"影子寄存器")。当一个段选择符被加载到一个 段寄存器可见部分中时,处理器也同时把段选择符指向的段描述符中的段地址、段限长以及访问控制信息加载 到段寄存器的隐藏部分中。缓冲在段寄存器(可见和隐藏部分)中的信息使得处理器可以在进行地址转换时 不再需要花费时间从段描述符中读取基地址和限长值。

由于影子寄存器含有描述符信息的一个副本,因此操作系统必须确保对描述符表的改动反映在影子寄存器中。 否则描述符表中一个段的基地址或限长被修改过,但改动却没有反映到影子寄存器中。 处理这种问题最简捷的方法是在对描述符表中的描述符作过任何改动之后就立刻重新加载6个段寄存器。 这将把描述符表中的相应段信息重新加载到影子寄存器中,并为加载段寄存器,提供了两类加载指令:

  • 像MOV、POP、LDS、LES、LSS、LGS以及LFS,这些指令显式地直接引用段寄存器。
  • 隐式加载指令,例如使用长指针的CALL、JMP和RET指令、IRET、INTn、INTO和INT3等指令。这些指令在 操作过程中会附带改变CS寄存器(和某些其他段寄存器)的内容。

MOV指令当然也可以用于把段寄存器可见部分内容存储到一个通用寄存器中。

段描述符

段的基地址、段限长以及段的保护属性存储在一个称为段描述符(Segment Descriptor)的结构项中。 在逻辑地址到线性地址的转换映射过程中会使用这个段描述符。段描述符保存在内存中的段描述符表 (Descriptor Table)中。段描述符表是包含段描述符项的一个简单数组。前面介绍的段选择符即用于通过 指定表中一个段描述符的位置来指定相应的段。

即使利用段的最小功能,使用逻辑地址也能访问处理器地址空间中的每个字节。 逻辑地址: 由16位的段选择符和32位的偏移量组成,如图所示:

https://github.com/novelinux/arch-x86/blob/master/res/logic2linear.jpg

段选择符指定字节所在的段,而偏移量指定该字节在段中相对于段基地址的位置。处理器会把每个逻辑地址 转换成线性地址。线性地址是处理器线性地址空间中的32位地址。与物理地址空间类似,线性地址空间也是 平坦的4GB地址空间,地址范围从0到0xFFFFFFFF。线性地址空间中含有为系统定义的所有段和系统表。

为了把逻辑地址转换成一个线性地址,处理器会执行以下操作:

  • 使用段选择符中的偏移值(段索引)在GDT或LDT表中定位相应的段描述符(仅当一个新的段选择符加载到段寄存器中时才需要这一步)。
  • 利用段描述符检验段的访问权限和范围,以确保该段是可访问的并且偏移量位于段界限内。
  • 把段描述符中取得的段基地址加到偏移量上,最后形成一个线性地址。

如果没有开启分页,那么处理器直接把线性地址映射到物理地址,即线性地址被送到处理器地址总线上。 如果对线性地址空间进行了分页处理,那么就会使用二级地址转换把线性地址转换成物理地址。

一般格式

前面我们已经说明了使用段选择符来定位描述符表中的一个描述符。段描述符是GDT和LDT表中的一个 数据结构项,用于向处理器提供有关一个段的位置和大小信息以及访问控制的状态信息。每个段描述符的 长度是8字节,含有3个主要字段:段基地址、段限长和段属性。 段描述符通常由编译器、链接器、加载器或者操作系统来创建,但绝不是应用程序。

下图给出了所有类型段描述符的一般格式:

https://github.com/novelinux/arch-x86/blob/master/res/descriptor.jpg

一个段描述符中各字段和标志的含义如下:

  • 段限长字段Limit(Segment limit field): 用于指定段的长度。处理器会把段描述符中两个段限长字段 组合成一个20位的值,并根据颗粒度标志G来指定段限长Limit值的实际含义。如果G=0,则段长度Limit范围 可从1B~1MB,单位是1B;如果G=1,则段长度Limit范围可从4KB~4GB,单位是4KB。

  • 基地址字段Base(Base address field): 该字段定义在4GB线性地址空间中一个段字节0所处的位置。处理器 会把3个分立的基地址字段组合形成一个32位的值。段基地址应该对齐16字节边界。虽然这不是要求的,但 通过把程序的代码和数据段对齐在16字节边界上,可以让程序具有最佳性能。

  • 段类型字段TYPE(Type field): 用行指定段或门(Gate)的类型、说明段的访问种类以及段的扩展方向。 该字段的解释依赖于描述符类型标志S指明是一个应用(代码或数据)描述符还是一个系统描述符。

TYPE字段的编码对代码、数据或系统描述符都不同,如下图所示:

https://github.com/novelinux/arch-x86/blob/master/res/type.jpg

  • 描述符类型标志S(Descriptor type flag): 用于指明一个段描述符是系统段描述符(当S=0)还是代码或数据段描述符(当S=1)。

  • 描述符特权级字段DPL(Descriptor privilege level): 用于指明描述符的特权级。特权级范围从0到3。 0级特权级最高,3级最低。DPL用于控制对段的访问。

  • 段存在标志P(Segment present): 用于指出一个段是在内存中(P=1)还是不在内存中(P=0)。 当一个段描述符的P标志为0时,那么把指向这个段描述符的选择符加载进段寄存器将导致产生一个段不存在 异常。内存管理软件可以使用这个标志来控制在某一给定时间实际需要把那个段加载进内存中。这个功能为 虚拟存储提供了除分页机制以外的控制。

  • D/B(默认操作大小/默认栈指针大小和/或上界限)标志:根据段描述符描述的是一个可执行代码段、下扩 数据段还是一个堆栈段,这个标志具有不同的功能。(对于32位代码和数据段,这个标志应该总是设置为1;对于16位代码和数据段,这个标志被设置为0。)

可执行代码段: 此时这个标志称为D标志并用于指出该段中的指令引用有效地址和操作数的默认长度。 如果该标志置位,则默认值是32位地址和32位或8位的操作数;如果该标志为0,则默认值是16位地址和16位或 8位的操作数。指令前缀0x66可以用来选择非默认值的操作数大小;前缀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。

代码和数据段描述符类型

当段描述符中S(描述符类型)标志被置位,则该描述符用于代码或数据段。此时类型字段中最高位 (第2个双字的位11)用于确定是数据段的描述符(复位)还是代码段的描述符(置位)。

对于数据段的描述符,类型字段的低3位(位8、9、10)被分别用于表示已访问A(Accessed)、可写W (Write-enable)和扩展方向E(Expansion-direction),参见下表中有关代码和数据段类型字段位的说明。 根据可写位W的设置,一个数据段可以是只读的,也可以是可读可写的。

https://github.com/novelinux/arch-x86/blob/master/res/code_data_type.png

堆栈段必须是可读/写的数据段。若使用不可写数据段的选择符加载到SS寄存器中,将导致一个一般保护异常。 如果堆栈段的长度需要动态地改变,那么堆栈段可以是一个向下扩展的数据段(扩展方向标志置位)。这里, 动态改变段限长将导致栈空间被添加到栈底部。

已访问位指明自从上次操作系统复位该位之后一个段是否被访问过。每当处理器把一个段的段选择符加载进 段寄存器,它就会设置该位。该位需要明确地清除,否则一直保持置位状态。该位可用于虚拟内存管理和调试。

对于代码段,类型字段的低3位被解释成已访问A(Accessed),可读R(Read-enable)和一致的C(Conforming)。 根据可读R标志的设置,代码段可以是只能执行、可执行/可读。当常数或其他静态数据以及指令码被放在了 一个ROM中时就可以使用一个可执行/可读代码段。这里,通过使用带CS前缀的指令或者把代码段选择符加载 进一个数据段寄存器(DS、ES、FS或GS),我们可以读取代码段中的数据。在保护模式下,代码段是不可写的。

代码段可以是一致性的或非一致性的。向更高特权级一致性代码段的执行控制转移,允许程序以当前特权级 继续执行。向一个不同特权级的非一致性代码段的转移将导致一般保护异常,除非使用了一个调用门或 任务门(有关一致性和非一致性代码段的详细信息请参见"直接调用或跳转到代码段")。不访问保护设施的 系统工具以及某些异常类型(例如除出错、溢出)的处理过程可以存放在一致性代码段中。需要防止低特权级 程序或过程访问的工具应该存放在非一致性代码段中。

所有数据段都是非一致性的,即意味着它们不能被低特权级的程序或过程访问。然而,与代码段不同, 数据段可以被更高特权级的程序或过程访问,而无须使用特殊的访问门。

如果GDT或LDT中一个段描述符被存放在ROM中,那么若软件或处理器试图更新(写)在ROM中的段描述符时, 处理器就会进入一个无限循环。为了防止这个问题,需要存放在ROM中的所有描述符的已访问位应该预先设置 成置位状态。同时,删除操作系统中任何试图修改ROM中段描述符的代码。

系统描述符类型

当段描述符中的S标志(描述符类型)是复位状态(0)的话,那么该描述符是一个系统描述符。处理器能够 识别以下一些类型的系统段描述符:

  • 局部描述符表(LDT)的段描述符;
  • 任务状态段(TSS)描述符;
  • 调用门描述符;
  • 中断门描述符;
  • 陷阱门描述符;
  • 任务门描述符;

这些描述符类型可分为两大类:系统段描述符和门描述符。系统段描述符指向系统段(如LDT和TSS段), 门描述符就是一个"门",对于调用、中断或陷阱门,其中含有代码段的选择符和段中程序入口点的指针; 对于任务门,其中含有TSS的段选择符。下表给出了系统段描述符和门描述符类型字段的编码。

https://github.com/novelinux/arch-x86/blob/master/res/sys_descriptor_type.png