操作系统篇-分段机制与GDT|LDT

Posted 卫卐

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统篇-分段机制与GDT|LDT相关的知识,希望对你有一定的参考价值。

 

|| 版权声明:本文为博主原创文章,未经博主允许不得转载。

  一、前言

    在《操作系统篇-浅谈实模式与保护模式》中提到了两种模式,我们说在操作系统中,其实大部分时间是待在保护模式中的。因此若想理解操作系统程序中的启动相关的部分,必须要理解保护模式下的编程,而分段机制是保护模式编程下的基础。而且,由于实模式与保护模式的不同,对保护模式下的分段机制更需要注意。
  二、线性地址
  在保护模式下编程,访问内存时,需要在程序中给出段地址和偏移量,因为分段是保护模式的基本特征之一。传统上,段地址和偏移地址称为逻辑地址,偏移地址叫做有效地址,在指令中给出有效地址的方式叫做寻址方式
  段的管理是由处理器的段部件负责进行的,段部件将段地址和偏移地址相加,得到访问内存的地址。一般来说,段部件产生的地址就是物理地址
  在分段模型下,内存的分配是不定长的,时间长了,内存空间就会碎片化,就有可能出现一种情况:内存空间是有的,但都是小块,无法分配给某个任务。为了解决这个问题,在支持分页功能后,分页功能将物理内存空间划分成逻辑上的页。页的大小是固定的,一般为 4KB,通过使用页,可以简化内存管理
  如下图所示,当页功能开启时,段部件产生的地址就不再是物理地址了,而是线性地址,线性地址还要经页部件转换后,才是物理地址。

 

   线性地址的概念用来描述任务的地址空间。如上图所示, 32位保护模式中,每个任务都拥有4GB 的虚拟内存空间,就像一段平直的线段,因此叫线性地址空间。相应地,由段部件产生的地址,就对应着线性地址空间上的每一个点,这就是线性地址
   三、段
  段是实现虚拟地址到线性地址转换机制的基础。在保护方式下,段的特征有以下三个:段基址,段限长,段属性。这三个特征存储在段描述符(segmentdescriptor)之中,用以实现从逻辑地址到线性地址的转换。段描述符存储在段描述符表之中,通常,我们使用段选择符定位段描述符在这个表中的位置。每个逻辑地址由16位的段选择符+32位的偏移量组成。
  段基址规定线性地址空间中段的开始地址。在保护模式下,段基地址长32位。因为基地址长度与寻址地址的长度相同,所以段基地址可以是 0~4GB 范围内的任意地址,而不像实方式下规定的边界必须被16整除。不过,还是建议应当选取那些 16 字节对齐的地址。尽管对于 Intel 处理器来说,允许不对齐的地址,但是,对齐能够使程序在访问代码和数据时的性能最大化。
  段界限规定段的大小。在保护模式下,段界限用20位表示,而且段界限可以是以字节为单位或以4K字节为单位。偏移量是从 0 开始递增,段界限决定了偏移量的最大值。对于向下扩展的段,如堆栈段来说, 段界限决定了偏移量的最小值。
  段属性我们放到下面讲解描述符的时候来讲。
  四、段描述符表
    和一个段有关的信息需要 8 个字节来描述,这就是段描述符( Segment Descriptor),每个段都需要一个描述符。为了存放这些描述符,需要在内存中开辟出一段空间。在这段空间里,所有的描述符都是挨在一起,集中存放的,这就构成一个描述符表。描述符表的长度可变,最多可以包含8K个这样的描述符(为什么呢?因为段选择子是16位的,其中的13bit用来作index)。
  有两种描述符表,GDT和LDT。结构如下:
   

 

   其实LDT与我们在《操作系统篇-浅谈实模式与保护模式》中提到过的GDT是差不多的,区别在于(1)全局(Global)和局部(local);(2)LDT表存放在LDT类型的段之中,此时GDT必须含有LDT的段描述符;(3)LDT本身是一个段,而GDT不是。
  查找GDT在线性地址中的基地址,需要借助GDTR;而查找LDT相应基地址,需要的是GDT中的段描述符。访问LDT需要使用段选择符,为了减少访问LDT时候的段转换次数,LDT的段选择符,段基址,段限长都要放在LDTR寄存器之中。

  对于操作系统来说,每个系统必须定义一个GDT,用于系统中的所有任务和程序。可选择性定义若干个LDT。GDT本身不是一个段,而是线性地址空间的一个数据结构GDT的线性基地址和长度必须加载进GDTR之中。因为每个描述符长度是8,所以GDT的基地址最好进行8字节对齐。

   五、段选择符
   实模式下的 6 个段寄存器 CS、 DS、 ES、 FS、 GS 和 SS,在保护模式下叫做段选择器。和实模式不同,保护模式的内存访问有它自己的方式。在保护模式下,尽管访问内存时也需要指定一个段,但传送到段选择器的内容不是逻辑段地址,而是段描述符在描述符表中的索引号。在保护模式下访问一个段时,传送到段选择器的是段选择符,也叫段选择子。其结构如下图所示:
 
   如图所示,段选择子由三部分组成,共16bit,第一部分是描述符的索引号,用来在描述符表中选择一个段描述符。 TI 是描述符表指示器, TI=0 时,表示描述符在 GDT 中; TI=1 时,描述符在 LDT 中。RPL 是请求特权级,表示给出当前选择子的那个程序的特权级别,正是该程序要求访问这个内存段。每个程序都有特权级别。
  GDT 的线性基地址在 GDTR 中,又因为每个描述符占 8 字节,因此,描述符在表内的偏移地址是索引号乘以 8。当处理器在执行任何改变段选择器的指令时(比如 pop、 mov、jmp far、 call far、 iret、 retf),就将指令中提供的索引号乘以 8 作为偏移地址,同 GDTR 中提供的线性基地址相加,以访问 GDT。如果没有发现什么问题(比如超出了 GDT 的界限),就自动将找到的描述符加载到不可见的描述符高速缓存部分。如下图所示:

 

 加载的部分包括段的线性基地址、段界限和段的访问属性。此后,每当有访问内存的指令时,就不再访问 GDT 中的描述符,直接用当前段寄存器描述符高速缓存器提供线性基地址。
   六、段描述符
   a.描述符的结构
  首先,我们来看看描述符的结构,拿出上篇blog的图来look一look:

  GDT的作用是用来提供段式存储机制,这种机制是段寄存器和GDT中的描述符共同提供的。每个描述符在GDT中占8字节,也就是 2 个双字,或者说是 64 位。图中,下面是低32位,上面是高32位。

   其中:
  G 位是粒度位,用于解释段界限的含义。当 G 位是“ 0”时,段界限以字节为单位。此时,段的扩展范围是从 1 字节到 1 兆字节( 1B~1MB),因为描述符中的界限值是 20 位的。相反,如果该位是“ 1”,那么,段界限是以 4KB 为单位的。这样,段的扩展范围是从 4KB到 4GB
  S 位用于指定描述符的类型( Descriptor Type)。当该位是“ 0”时,表示是一个系统段;为“ 1”时,表示是一个代码段或者数据段(堆栈段也是特殊的数据段)。
   DPL 表示描述符的特权级( Descriptor Privilege Level, DPL)。这两位用于指定段的特权级。共有 4 种处理器支持的特权级别,分别是 0、 1、 2、 3,其中 0 是最高特权级别, 3 是最低特权级别。刚进入保护模式时执行的代码具有最高特权级 0(可以看成是从处理器那里继承来的),这些代码通常都是操作系统代码,因此它的特权级别最高。每当操作系统加载一个用户程序时,它通常都会指定一个稍低的特权级,比如 3 特权级。不同特权级别的程序是互相隔离的,其互访是严格限制的,而且有些处理器指令(特权指令)只能由 0 特权级的程序来执行,为的就是安全。这里再次点明了为何叫保护模式。
   P 是段存在位( Segment Present)。 P 位用于指示描述符所对应的段是否存在。一般来说,描述符所指示的段都位于内存中。但是,当内存空间紧张时,有可能只是建立了描述符,对应的内存空间并不存在,这时,就应当把描述符的 P 位清零,表示段并不存在。P 位是由处理器负责检查的。每当通过描述符访问内存中的段时,如果 P 位是“ 0”,处理器就会产生一个异常中断。
   D/B 位是“默认的操作数大小”( Default Operation Size)或者“默认的堆栈指针大小”,又或者“上部边界”标志。设立该标志位,主要是为了能够在 32 位处理器上兼容运行 16 位保护模式的程序。D=0 表示指令中的偏移地址或者操作数是 16 位的; D=1,指示 32 位的偏移地址或者操作数。
   举个例子来说, 如果代码段描述符的 D 位是 0,那么,当处理器在这个段上执行时,将使用 16位的指令指针寄存器 IP 来取指令,否则使用 32 位的 EIP。
  对于堆栈段来说,该位被叫做“ B”位,用于在进行隐式的堆栈操作时,是使用 SP 寄存器还是ESP 寄存器。
  L 位是 64 位代码段标志,保留此位给 64 位处理器使用。目前,我们将此位置“ 0”即可。
  TYPE 字段共 4 位,用于指示描述符的子类型,或者说是类别。
  b.描述符类型
  对于数据段来说, 这 4 位分别是 X、 E、 W、 A 位;而对于代码段来说,这 4 位则分别是 X、 C、 R、 A 位。如下表所示

 
 
   X 表示是否可以执行( eXecutable)。数据段总是不可执行的, X=0;代码段总是可以执行的,因此, X=1。
  对于数据段来说, E 位指示段的扩展方向。 E=0 是向上扩展的,也就是向高地址方向扩展的,是普通的数据段; E=1 是向下扩展的,也就是向低地址方向扩展的,通常是堆栈段。
  W 位指示段的读写属性,或者说段是否可写, W=0 的段是不允许写入的,否则会引发处理器异常中断; W=1的段是可以正常写入的。
  对于代码段来说, C 位指示段是否为特权级依从的( Conforming)。 C=0 表示非依从的代码段,这样的代码段可以从与它特权级相同的代码段调用,或者通过门调用; C=1 表示允许从低特权级的程序转移到该段执行。
   R 位指示代码段是否允许读出。代码段总是可以执行的,但是,为了防止程序被破坏,它是不能写入的。至于是否有读出的可能,由 R 位指定。 R=0 表示不能读出,如果企图去读一个 R=0 的代码段,会引发处理器异常中断;如果 R=1,则代码段是可以读出的,即可以把这个段的内容当成 ROM 一样使用。
  也许有人会问,既然代码段是不可读的,那处理器怎么从里面取指令执行呢?事实上,这里的R属性并非用来限制处理器, 而是用来限制程序和指令的行为
   数据段和代码段的 A 位是已访问位,用于指示它所指向的段最近是否被访问过。在描述符创建的时候,应该清零。之后,每当该段被访问时,处理器自动将该位置“ 1”。
   七、总结
  GDT|LDT是进入保护模式必须了解的基础之一,其是保护模式中进行寻址的关键,也是以后理解代码跳转的基础,必须要熟练。

以上是关于操作系统篇-分段机制与GDT|LDT的主要内容,如果未能解决你的问题,请参考以下文章

Linux的内存管理

GDT,LDT,GDTR,LDTR

Selector Descriptor

(LDT的使用)

(LDT的使用)

GDT临时分段