L2 对GDT与LDT的理解


在学习操作系统的过程中,由于没有充足的预备知识,学习起来举步维艰。这是我在学习GDT、LDT时的一些学习笔记,是对网上资料的一些整理,主要是用于学习和交流,错误之处希望大家指出 😃。

1 内存寻址

1.1 实模式下的内存寻址

实模式(也就是 8086 的模式)下的寻址方式如下:

段首地址 × 16 + 偏移量 = 物理地址

乘以 16 是因为在 8086 CPU中,地址线是20位,但寄存器是16位的(或者说用来寻址的段寄存器只有16位),最高寻址64KB,它无法寻址到1M内存。于是,Intel 设计了这种寻址方式,这也造成了段的首地址必须是16的倍数的限制。

1.2 保护模式下分段机制的内存寻址

保护模式和实模式的寻址方式有着很大的不同。保护模式下内存寻址的方式为:利用段选择子(通常情况下也就是实模式下的段寄存器:cs、ds等)从段描述符表(GDT、LDT都是段描述符表)中找到对应的段描述符,因为段描述符中含有需要寻址的段基地址、段长度,段属性等信息,所以利用通过段选择子找到的段描述符和偏移量就可以寻址到对应的内存地址。在未开启分页机制的情况下,此时保护模式下的寻址方式如下:

段描述符中的段基址 + 偏移量 = 物理地址

这一段话,出现了三个新名词:

段选择子 ; 段描述符表 ; 段描述符

可以先简单理解:段描述符表 是一个数组,这个数组中的每一项都是一个 段描述符,段选择子 作为 段描述符表 的索引,其实 段选择子 就是数组的下标

我们再看一下实模式下和保护模式下 ,下面两条汇编指令的区别(下面两句代码都来自 Linux0.11 ):

jmpi go, INITSEG ! 该指令在实模式下执行, go是个汇编标号,是偏移地址,INITSEG 是段基地址(在mov指令中段基地址
                 ! 通常是cs、ds等)。所以该指令要跳转 INITSEG << 4 + go 地址去执行。
                 
jmpi 0, 8        ! 该指令在保护模式下执行,0 为偏移地址,8 为段选择子(在mov指令中段选择子通常是cs、ds等)。
                 ! 所以该指令利用 8 找到对应的段描述符,在利用段描述符中的段基地址 + 偏移地址找到对应的物理地址。

2 什么是GDT

GDT 就是段描述符表的一种。GDT 全名是全局描述符表,存放在内存中(GDT可以被放在内存的任何位置),只有一张且全局可见。经过上一节的分析,GDT 就是一个段描述符类型的数组。形象点表示的话,如下:

//Discriptor就是段描述符,该结构体大小为 64 bit
struct Discriptor {
    elemType Base;        //段物理首地址
    elemType Limit;       //段界限
    elemType Access;      // 段属性
};
//discriptorTable[n] 就是描述符表(GDT)
struct Discriptor  discriptorTable[n] = {...};

段描述符 有64位,段描述符的实际结构如下(其实段描述符不是上面说的那个结构体,不过内容也没什么区别。另外段描述符为什么会这么奇怪,好像是为了兼容之前的架构):
图2.1 段描述符

图2.1 段描述符

从图中可以看出 段描述符表 中有一个重要的信息——段基地址。关于 段属性 可以简单了解一下:
1、G:
G=0时,段限长的20位为实际段限长,最大限长为2^20=1MB
G=1时,则实际段限长为20位段限长乘以2^12=4KB,最大限长达到4GB

2、DPL:特权级,0为最高特权级,3为最低,表示访问该段时CPU所需处于的最低特权级。( 区别一下CPL:CPL是当前进程的权限级别(Current Privilege Level),是当前正在执行的代码所在的段的特权级,存在于cs寄存器的低两位。)

3 如何找到GDT

前面介绍到GDT是存放在内存某一处的数组,那么我们如何找到它呢?Intel 的设计者提供了一个寄存器GDTR(48 bit)用来存放GDT的入口地址,程序员将 GDT 设定在内存中某个位置之后,可以通过 LGDT 指令将 GDT 的入口地址装入此寄存器,从此以后,CPU 就根据此寄存器中的内容作为 GDT 的入口来访问 GDT 了。GDTR 中存放的是 GDT 在内存中的基地址和其表长界限。GDTR 结构如下:

图3.1 GDT结构图

图3.1 GDT结构图

从图中可以看出 :GDTR 的低 16 bit 为 GDT 的表界限,高32 bit 为 GDT的基地址。
基地址指定 GDT 表 第0号字节的地址,表长界限指明 GDT 表的字节长度值。指令 LGDT 和 SGDT 分别用于加载和保存 GDTR寄存器的内容。在机器刚加电或处理器复位后,基地址被默认地设置为0,而长度值被设置成0xFFFF,因此在保护模式初始化过程中必须给GDTR加载一个新值,即操作系统在启动保护模式前先要做好 GDT 表,并将 GDT 。下面是 Linux0.11 加载 GDTR 的一条汇编指令:

lgdt	gdt_48		! load gdt with whatever appropriate (将GDT 的地址存在到 GDTR 中)

4 什么是LDT

LDT 的全称是局部段描述符表。LDT 的结构与 GDT 类似,也存放在内存中,与GDT不同的是,LDT在系统中可以存在多个(当然LDT也可以没有),并且从LDT的名字可以得知,LDT不是全局可见的,它们只对引用它们的任务可见,每个任务最多可以拥有一个LDT。另外,每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中

IA-32 为 LDT 的入口地址也提供了一个寄存器 LDTR,因为在任何时刻只能有一个任务(或者说进程)在运行,所以 LDT 寄存器全局也只需要有一个。如果每个任务都拥有自身的 LDT,那么在切换到下一个任务的时候,就要修改 LDTR ,让 LDTR 指向下一个任务的 LDT。修改方式就是通过 LLDT 指令将下一个任务的 LDT 的 段描述符的索引(也就是段选择子)装入此寄存器。
图4.1 Linux0.11中的 GDT 和 LDT

图4.1 Linux0.11中的 GDT 和 LDT

LLDT 指令与 LGDT 指令不同:

  • LGDT 指令的操作数是一个 32-bit 的内存地址,这个内存地址指向一个48bit的内存空间,这个内存空间里存放了 32-bit GDT 的入口地址,以及 16-bit 的GDT Limit。LGDT 指令会将这48bit内容加载到 GDTR中。
  • LLDT 指令的操作数是一个16-bit的 选择子,这个选择子主要内容是:被装入的 LDT 的段描述符在 GDT 中的索引值。LLDT 指令会将这个16-bit的 选择子加载到 LDTR 中。

5 段选择子

原先实模式下的各个段寄存器作为保护模式下的段选择子(也可以称为段选择器、段选择符),80486中有6个(即CS,SS,DS,ES,FS,GS)16位的段寄存器。段选择子 CS 对应表示的段仍为代码段,SS 对应表示的段仍为堆栈段。当然段选择子也不都是段寄存器,而是根据具体的汇编指令而定,就像第1节中的那个 jmpi 指令,选择子就是一个常数

段选择子大小为 16bit。段选择子包括三部分:描述符索引(index)、TI、请求特权级(RPL)。
图5.1 段选择子结构

图5.1 段选择子结构

(图5.1存在一些错误:段选择子的大小应该是0~15 共 16bit,其中 RPL 为 bit0 - bit1、TI 为 bit2、Index 为 bit3 - bit15)

  • Index(描述符索引)部分表示所需要的段描述符在描述符表的位置(如Index = 1,表示段描述符表的第1项段描述符),由这个位置再根据在 GDTR 中存储的描述符表基址就可以找到相应的描述符。然后用描述符表中的段基址加上逻辑地址(SEL:OFFSET)的OFFSET就可以转换成线性地址。
  • TI 值只有一位0或1,0代表选择子是在 GDT 中索引,1代表选择子是在 LDT 中索引。
  • 请求特权级(RPL)则代表选择子的特权级,共有4个特权级(0级、1级、2级、3级)。

关于特权级的说明:
任务中的每一个段都有一个特定的级别。每当一个程序试图访问某一个段时,就将该程序所拥有的特权级与要访问的特权级进行比较,以决定能否访问该段。系统约定,CPU只能访问同一特权级或级别较低特权级的段。

Linux0.11中只是用了2个特权级:0级和3级,其中操作系统的内核区为0级,用户区为3级。特权级有3种类型:CPL、DPL和RPL。

  • CPL为当前特权级,表示当前正在执行的程序的特权级,存放在cs和ss寄存器中。int指令可以将 CPL 改为0
  • DPL为描述符特权级,表示要访问的段或门的特权级(目标特权级)。DPL存放在段描述符或门描符中。
  • RPL为请求特权级,存放在段选择子(如ds,cs等段寄存器,不过RPL通常被用在ds中)中。若这个段选择子是 cs,那么此时RPL 就等于 CPL (因为CPL只存放在 cs 中)。

当程序访问一个段时,处理器会检查CPL、DPL和RPL, 只有当 DPL>= CPL 且 DPL>= RPL 时处理器才会允许程序访问该段。例如 CPL = 3,RPL = 3, DPL = 0 时程序访问的请求时不被允许的。

6 再看保护模式下分段机制的内存寻址(实例对理解非常有用)

6.1 访问 GDT

图6.1 通过访问 GDT 寻址

图6.1 通过访问 GDT 寻址

TI = 0时表示段描述符在 GDT 中,如上图所示:

  1. 先从 GDTR 寄存器中获得 GDT 基址;
  2. 然后再根据段选择子(段选择器)高13位的位置索引值得到段描述符;
  3. 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址;

6.2 访问 LDT

图6.2 通过访问 LDT 寻址

图6.2 通过访问 LDT 寻址

TI = 1 时表示段描述符在 LDT 中,如上图所示:

  1. 还是先从 GDTR 寄存器中获得 GDT 基址;
  2. 从 LDTR 寄存器中获取 LDT 所在段的位置索引 (LDTR高13位),此时 LDTR 可以看成一个段选择子;
  3. 通过这个位置索引 (LDTR高13位),在 GDT 中得到 LDT 的段描述符从而得到 LDT 段基址;
  4. 利用段选择子(段选择器)高13位位置索引值从 LDT 段中得到需要访问的内存的段描述符;
  5. 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址 yyyyyyyy 得到最后的线性地址;

7 总结

其实GDT只是一个段描述符类型的数组,它主要是用于内存寻找。为了对部分内存区域(存放着核心数据)进行保护,因此将段描述符设计成了三部份。而LDT的结构和GDT类似,我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。

LDT不包含在GDT中。GDT中只是包含了LDT描述符(一个指向LDT起始地址的指针)

《Linux内核完全剖析——基于0.12内核》中有一个实验:一个简单多任务内核实例,这个实验对理解保护模式下的分段机制有很大帮助。

参考

本章内容中的图6.1、图6.2来源于网络,图2.1、图4.1截取自《Linux内核完全剖析——基于0.12内核》。

[1] GDT、GDTR、LDT、LDTR的学习
[2] GDT,LDT,GDTR,LDTR 详解,包你理解透彻
[3] 两张图看懂GDT、GDTR、LDT、LDTR的关系
[4] 保护模式下寻址
[5] 《Linux内核完全剖析——基于0.12内核》

  • 7
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值