首页
社区
课程
招聘
[原创]3.滴水中级班(内核驱动)——段描述符与段选择子
发表于: 2025-9-19 22:22 510

[原创]3.滴水中级班(内核驱动)——段描述符与段选择子

2025-9-19 22:22
510

先介绍两张表,分别是全局描述符表GDT、局部描述符表LDT,全称分别是Global Descriptor Table和Local Descriptor Table。CPU查表,查哪张表、查什么位置、查多少数据都是由AX的值来决定的,所以AX的值是不能随便写的。LDT这张表在Windows中没有使用,所以我们做实验的时候主要是查GDT这张表。

这里我们还需要再记住一个寄存器——GDTR,这个寄存器里面存了两个值,一个值是GDT这张表在哪里,另一个值是这张表有多大。这里不要弄混:GDT是一张表,GDTR是一个寄存器,GDTR存储的是表的开始位置和长度。我们想要查看GDT这张表,那么就要先知道它在哪里,这里先记一下一个指令:

r gdtr

这里的r指令就是查看寄存器的意思,后面跟的就是寄存器的名字,这里的gdtr注意是48位不是96位的,48位中有32位存储的就是这张表在哪里。

看GDT这张表有多大,使用指令

r gdtl

gdtl实际上查看到也是gdtr这个寄存器,只是查看的是gdtr寄存器的另外一个值——16位的,指明的就是GDT这张表有多大。

再学习一个指令——dd指令

dd 8003f000

这个指令打印出来的结果中,左侧竖着的一列就是我们给出的打印出来的内存的地址,后面的四组就是里面的数据,为了方便显示所以一行显示了十六个字节,所以后面的十六个字节才是真正的数据。我们现在看到的这些数据就是GDT表里面的数据。

接下来介绍一个概念叫段描述符。

段描述符是什么意思呢?大家记得当我们执行MOV指令的时候,它要从下面这张表里面去查询数据

把查出来的数据(段寄存器有96位,但是我们只给了16位,那剩下的值就是从这张表里查出来然后放进去),查出来多少数据呢,每次从里面查询8个字节,因为GDT表里面存储的每个元素我们称之为段描述符。GDT是一张表,它里面存储的每一个元素被称为段描述符,每一个段描述符是多大呢?是8个字节。也就是说每隔两组就是一个段描述符,比如上面的 00000000 00000000是一个段描述符、0000ffff、00cf9b00是一个段描述符,以此类推,就是第一个、第二个、第三个......短描述符。

注意了这个表必须背会!可以不理解什么含义,但是必须背下来,否则后面讲的时候概念特别多越讲越乱就听不懂了。

这里再学习一个指令叫dq指令,我们知道dd指令后面的d是dword的意思,也就是查询出来的数据每四个字节为一组展示出来。那么dq就是qword,也就是八个字节为一组展示出来

dq

如果用dq来查看,是把八个字节分为了一组(因为我们之前见过了段描述符是八个字节为一组的,所以以后再查看GDT表的时候,一般都用dq指令了),并且注意这里有一个符号`,这个符号的意思就是用作地址分隔符(模块限定符)

我们用dq去查看某个地址的内存的时候,它会把高位放到前面,低位放到后面,和我们的小端存储是一样的。

如图所示,我们这个图的上面就是高四个字节,下面就是低四个字节。一定要先弄清楚它们之间的对应关系,然后把这个表是什么样的给记下来。

这里再提一嘴,我们知道刚才的r寄存器指令使得我们查询的时候,知道了gdtl是3ff这么长,但是我们用dq打印出来的时候,却非常短,显然不符合3ff这么长的情况。

原来啊,咱们的这个显示是没有显示全,我们可以通过在打印命令后缀加上 L数字的形式来打印出来对应数字的组数

dq 地址 L数字

其中数字是十六进制,如下图所示就表示打印出来了0x40组数据

那么回到开头,当我们通过MOV指令去执行段寄存器赋值操作的时候,CPU查表中查的位置由AX指定,如果一旦查到了,那么查出来的段描述符,就把这八个字节的值取出来放到段寄存器里,这个才是这张段描述符表完整的含义。

所以这就是为什么明明给的是16位,最后写的时候却给了96位。

我们接下来还要进一步分析,这个值决定了在这个表的什么位置,因为每一个段描述符有八个字节,我们的AX决定了到底用哪一个段描述符,如果我们想弄清楚这一点,还要去分析一个新的概念就是——段选择子。

段选择子是一个16位的数,就是一个单纯的数字,不要和别的联系到一起。这16位决定了我们到GDT表里面去查哪一个段描述符,这16位的结构如上图所示。我们现在需要记住的就是每个东西的位置、长度和意思就OK了。

第一部分的第0位和第1位的RPL是请求特权级别;第二部分的2就是TI位,当TI=0的时候查的是GDT表、TI=1的时候查的是LDT表;我们刚才介绍过当MOV DS,AX的时候,它实际上是要查表的,要么是GDT表,要么是LDT表,因为Windows中没有使用LDT表,所以我们无论是自己做实验还是看别人写东西的时候,这个TI位永远都是0,这就是TI位的含义。第三部分的Index是从第3到第15位,是一个索引,我们查询GDT表里的哪一个段描述符就是由这个Index索引拼出来的数字决定的。

这里举个例子比如这里的1B,由于1B是十六进制,所以应该是0x1B,而又由于段选择子是16位也就是两个字节,也就是四个十六进制数,所以按道理来说直观点我们在前面要补充00,也就是0x001B,那么拆分成二进制数就是0000 0000 0001 1011。那么我们可以去对应位去找,RPL就是11,TI就是0表示查GDT表而不是LDT表,剩下的就是0000 0000 00011也就是3,所以表示查询段描述符索引的3这个位置的,那么从0开始查,就可以查询到第二张图那个被蓝色选中的部分了。

那么1B对应的段描述符的位置就是上面这个蓝色的位置,这就是段选择子的含义。也就是说我给的16位的数不是随便写的,也是有其含义的。

我们之前是用MOV指令加载过段寄存器DS、FS等等的,那么除了MOV指令,我们还可以使用如下的指令去修改寄存器。

LDTR和TR这两个寄存器后面也会单独讲。注意这里使用这些指令的时候,后面要先给一个[buffer]指针地址,fword代表六个字节,qword是八个字节,dword是四个字节,这个les指令的意义就是把[buffer]的高两个字节赋值给es,因为这是les指令,之后就是把低四个字节赋值给ecx,这就是这个指令的含义。es不能随便给,段选择子是16位也就是俩字节,这正好是能对应上了。

接下来注意到一个复杂的地方了,就是我们“加载段描述符至段寄存器”这里,需要注意RPL(请求特权级)<=DPL(描述符特权级),这里的RPL就是我们段选择子的那个最低两位的RPL,这里的DPL是我们段描述符(这八个字节的段描述符)里的高四个字节里面的第13位、第14位的DPL。

我们在用les这个指令做实验的时候,一定要确保RPL<=DPL,否则指令无法正确执行,即会失败。

CPU通过段描述符(64位/8字节)来填充段寄存器的80位不可见部分,但段描述符本身只有8字节64位,这显然不够,因此CPU有一套明确的扩展规则来填充完整的80位(32位基址+32位段限长+16位属性),并且这个过程还依赖于段描述符中的G位和P位进行补位和扩展。

填充过程详解

  • 段寄存器的结构:每个段寄存器实际有96位,其中16位可见(段选择子),剩下的80位由段描述符的内容扩展得到。

  • 段描述符的内容:包含段基址段限长(Limit)、段属性(包括可读、可写、可执行、特权级等)。

  • 补位规则

    • 段基址:段描述符中基址分三部分存储(8+8+16),组合成一个32位地址,直接填充到段寄存器的32位Base字段。

    • 段限长:段描述符中Limit只有20位,但段寄存器需要32位。此时,根据G位补零或补F。若G=0,Limit单位为字节,扩展为32位时高位补0;若G=1,Limit单位为4KB页,扩展为32位时低12位补F(即从20位扩展到32位,低12位为1,其余位不变)。

    • 段属性:16位属性直接从段描述符中对应的位域截取,包括Type、S、DPL、P、AVL、G等。

关键位影响

  • G位(Granularity,粒度):决定Limit是否需要扩展。G=0时,Limit单位为字节,段限长直接使用;G=1时,段限长需要在原有基础上乘以4KB,也就是在二进制末尾加12个0,但段寄存器的Limit字段直接存储的是最后的32位地址(即低位补F)。

  • P位(Present):指示段是否有效,无效时CPU不会加载该段描述符。

举例

假设段描述符中Limit=0xFFFFF(20位),G=0时,段寄存器中的Limit为0x000FFFFF;G=1时,为0xFFFFFFFF(即0xFFFFF << 12 | 0xFFF)

总结表

字段段描述符来源段寄存器填充规则
基址(Base)三个部分拼合成32位直接填入
限长(Limit)20位,G位扩展G=0:高位补0;G=1:低12位补F(相当于4095页最大4GB)
属性从段描述符位域提取直接填入

核心结论

段寄存器80位不可见部分,是由段描述符的64位内容,按照CPU定义的规则(尤其依赖G位和P位)扩展得来的。这个过程由硬件自动完成,程序员只需正确设置GDT表项,CPU会根据段选择子访问描述符并完成填充。



[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回