段页这些概念是CPU的,不是操作系统的,CPU提供了这些功能,但是操作系统可以选择使用,也可以选择不适用。所谓保护模式,主要指段页的保护,尤其是段的保护,我们写段寄存器的时候,只给了16位,剩下80位并未给出,其实这80位的数据将通过查 GDT 表或者 LDT 表来获得。GDT 表和 LDT 表实际上就是一个大数组,数组中的每一项占用 8 个字节。

1.GDT,LDT,IDT表

GDT是“global(segment) descriptor table”的缩写,也就是全局段描述符表。这些64kb数据整齐的排列在内存中某一位置。而该位置的内存地址以及有效的个数就存放在GDTR中。GDTR是一种特殊的寄存器。

LDT(Local Descriptor Table,局部描述符表),但与GDT不同的是,LDT在系统中可以存在多个,并且从LDT的名字可以得知,LDT不是全局可见的,它们只对引用它们的任务可见,每个任务最多可以拥有一个LDT。另外,每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中。LDT只是一个可选的数据结构,操作系统(或者底层开发者)完全可以不用它。使用它或许可以带来一些方便性,但同时也带来复杂性,如果你想让你的OS内核保持简洁性,以及可移植性,则最好不要使用它。

IDT是“interrupt descriptor table”的缩写,是中断描述符表。IDT记录了0~255的中断号码和中断服务函数的关系。当发生中断的时候,通过中断号码去执行中断服务函数。

Windows系统只用了CPU的2张表:GDT,IDT,Windows没使用LDT

2.GDTR和LDTR寄存器

GDT可以被放在内存的任何位置,那么当程序员通过段寄存器来引用一个段描述符时,CPU必须知道GDT的入口,也就是基地址放在哪里,所以Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。

IA-32为LDT的入口地址也提供了一个寄存器LDTR,因为在任何时刻只能有一个任务在运行,所以LDT寄存器全局也只需要有一个。如果一个任务拥有自身的LDT,那么当它需要引用自身的LDT时,它需要通过LLDT指令将其LDT的段描述符装入此寄存器。LLDT指令与LGDT指令不同的时,LGDT指令的操作数是一个32-bit的内存地址,这个内存地址处存放的是一个32-bit GDT的入口地址,以及16-bit的GDT Limit。而LLDT指令的操作数是一个16-bit的选择子,这个选择子主要内容是:被装入的LDT的段描述符在GDT中的索引值。

3.段描述符

段寄存器一共有 96 位,其中 16位 可见部分来源于段选择子的索引部分。剩下 80 位来源于 GDT 表。大小是 96 位,可以抽象成以下结构

struct SegMent {
    WORD selector;
    WORD attribute;
    DWORD base;
    DWORD limit;
}
selector: 段选择子的索引部分
attribute: attribute 属性记录了该段是否有效,是否可读可写可执行等权限
base:基地址,如:ds:[0x00012345]有效地址就是ds.base+0x123456得出具体地址
limit:标识这个段有多大,越界访问会报错

4.段选择子

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

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

怎样查看程序处于哪个运行级别? 用OD打开一个 exe 程序,然后观察 cs 段寄存器的值。只看可见部分,剩余的80位隐藏位我们还不用关心。取cs寄存器的低2个bit位,转成十进制,就是对应的特权级别(0环~3环),值如下:

00-->0环
01-->1环
10-->2环
11-->3环

0环、1环、2环、3环,这四个CPU特权级别是CPU提供的,操作系统一般用不到这么多,如:windows系统只用到了0环和3环,1环和2环并没有使用。如下图的CS=0023,则说明当前程序运行在3环,它也有个名字,叫做当前运行特权级(CPL)

红框内是读/写/执行权限,黄框内是基址(base地址),绿框内是长度范围

5.练习

5.1如何把 0x1B 、0x23 这两个选择子对应的描述符填充到段寄存器

原始数据:

|--地址--|-------------16进制值---------------| 8003f000 0000000000000000 00cf9b000000ffff 8003f010 00cf93000000ffff 00cffb000000ffff 8003f020 00cff3000000ffff 80008b04200020ab 8003f030 ffc093dff0000001 0040f30000000fff

(1)0x1B

0x1B = 0000 0000 0001 1011b
索引号:0000 0000 0001 1= 3 (查找gdt[3])
RPL: 11b = 3
TI: 0 (查找 GDT 表)
查找到的 GDT 描述符为:gdt[3] = 00cffb00`0000ffff
段寄存器结构:
selector  = 0x001B
attribute = 0xcffb (G = 1 DB = 1 P = 1 DPL = 3 S = 1 TYPE = 1011(非一致代码段,可读已访问过))
base      = 0x00000000
limit     = 0xffffffff

(2)0x23

0x23 = 0000 0000 0010 0011b
索引号:0000 0000 0010 0 = 4
TI: 0 (查找 GDT 表)
RPL: 11b = 3
查找到的 GDT 描述符为:gdt[4] = 00cff300`0000ffff
段寄存器结构:
selector  = 0x23
attribute = 0xcff3 (G = 1 DB = 1 P = 1 DPL = 3 S = 1 TYPE = 0011(可读可写向上扩展的数据段))
base      = 0x00000000
limit     = 0xffffffff

其中,Limit算法如下:

如果 G = 0,把段描述符中的 20 bit LIMIT取出来,比如 0x003ff,然后在前面补 0 至32bit,即 limit = 0x000003ff.
如果 G=1,把段描述符中的 20 bit LIMIT取出来,比如 0x003ff,然后在后面补 f 至 32bit, 即 LIMIT = 0x003fffff

5.2把逻辑地址转为线性地址

例如给出逻辑地址:21h:12345678h 转换为线性地址的步骤如下: (1)、选择子SEL=21h=0000000000100 0 01b 它代表的意思是:选择子的index=4即选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;最后的01代表特权级RPL=1 (2)、OFFSET=12345678h若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h

总结

段选择子就是一个数值,放入DS、CS段寄存器,去GDT表里面选择段地址值,GDT表里面的的数值就是段描述符,里面有大量对该段的属性描述。

参考: https://blog.csdn.net/wrx1721267632/article/details/52056910 https://blog.csdn.net/q1007729991/article/details/52538080