操作系统——内存段的保护(七)

操作系统——内存段的保护(七)

2020-09-19 09:12:41 hawk


概述

  因为前面已经大体介绍了一下GDT中段描述符所设置的内存段的属性位,但是对于其效果和保护检查还不是很清楚。这里再次通过实验进行测试和验证,方便我们更加深入的理解GDT的作用。


段寄存器中选择子保护

判断是否超越GDT的界限

  正如前面分析过的,选择子的高13位是段描述符的索引值,第0~1位是RPL,第2位是TI位。对于RPL和TI位,实际上并不能进行明显的检查,因为其只有在运行的时候,才能体现出错误,单纯的静态分析无法分析出相关的错误。因此可以直接静态检查的只有选择子的索引值。cpu需要保证选择子的索引值一定要小于等于描述符表的界限,无论是GDT亦或是LDT,后面我们就默认在GDT中。

  我们知道,每一个段描述符大小是8字节,因此实际上选择子对应的边界也就是

描述符表基地址 + 选择子中的索引值 * 8 + 7

  

  而描述符表的边界可以直接通过相关的寄存器获取,结果如下所示

描述符表基地址 + 描述符表界限值

 

  因此cpu只要确保选择子对应的边界小于等于描述符表的边界即可,即

选择子中的索引值 * 8 + 7 <= 描述符表界限值

 

  我们在loader进入保护模式后尝试修改其源代码,使其加载超过描述符表界限值(0x1f)的选择子(不妨就设置为0000000000100_0_00b)。源代码修改如下所示

PROTECTION_MODE_START:

    mov    ax, GDT_SECT_DATASTACK
    mov    ds, ax
    mov    gs, ax
    mov    ss, ax                ;初始化各个段寄存器,将其都指向GDT_SECT_DATASTACK段描述符对应的段

    mov    esp, LOADER_STACK_TOP
    mov    ax, GDT_SECT_VIDEO
    mov    es, ax                ;这里将es设置为GDT_VIDEO段描述符对应的段,即显存段,那里没有使用平坦模式,访问显存仍然类似于实模式




    jmp    0000000000100_0_00b:0

 

  结果如下所示

 

   可以看到,我们将代码段寄存器中的选择子的索引值更新为4后,当执行完这条指令后,cpu会根据根据更新的段代码寄存器的值去GDT中获取相关的段描述符,从而cpu会检测到异常的索引值,最终抛出错误。

判断是否引用GDT的第0个段描述符

  前面也分析过了,为了防止段寄存器没有被初始化,因此设置GDT中的第0个段描述符不可被访问(对于LDT则没有这种限制)。这里需要说明的是,在保护模式下,CS段寄存器和SS段寄存器是直接不可以加载索引值为0的GDT的选择子(对于cs段寄存器,因为cpu执行下一条指令会用到cs段寄存器,因此如果更改后会立马被检测到;对于ss段寄存器的原因还不是很明白);对于其他段寄存器来说(ds、es、fs、gs段寄存器),虽然可以加载索引值为0的GDT的选择子,但是在真正使用到这些段寄存器的时候,cpu仍然会抛出异常,从而导致错误。我们首先尝试修改ss段寄存器的选择子,源代码如下所示

[bits 32]
PROTECTION_MODE_START:
    mov    ax, 0000000000000_0_00b
    mov    ss, ax

 

  最后执行的结果如图所示

 

 

  可以看到,对于ss段寄存器、cs段寄存器等,确实直接不让装载索引值为0的选择子,装载的话cpu直接会跑出异常。下面我们在尝试一下es段寄存器,源代码如下所示

 

   可以看到,对于ds、es、fs和gs段寄存器来说,可以加载索引值为0的选择子,但是当其真正进行寻址的时候,cpu会抛出异常,仍然相当于无法使用GDT的第0个段描述符。

检查段类型

  我们知道,段描述符中包含TYPE字段和S字段,其共同表示段的用途和类型。自然的,当我们在操作的时候,cpu会对这些段的属性进行检查,确保符合一些固定的规则,大体规则如下所示

1.    只有具备可执行属性的段(代码段)才能加载到cs段寄存器中
2.    只具备执行属性的段(代码段)不允许加载到除cs外的段寄存器中
3.    只有具备可写属性的段(数据段)才能加载到ss栈段寄存器中
4.    至少具备可读属性的段才能加载到ds、es、fs、gs段寄存器中

 

  形象化一下上面的规则,如下表所示

段寄存器 代码段(X=1) 数据段(X=0)
只执行(R=0) 执行+可读(R=1) 只读(R=1,W=0) 读写(R=1,W=1)
CS 通过检查 通过检查 不通过检查 不通过检查
DS 不通过检查 通过检查 通过检查 通过检查
ES 不通过检查 通过检查 通过检查 通过检查
FS 不通过检查 通过检查 通过检查 通过检查
GS 不通过检查 通过检查 通过检查 通过检查
SS 不通过检查 不通过检查 不通过检查 通过检查

 

  这里由于规则比较多,我们就选取两条具有代表性质的规则进行检验即可,首先是cpu对于ES段寄存器加载只执行代码段的段描述符的检查(因为之前我一直没搞懂对于代码段来说什么是可执行,什么是可读);其次测试cpu对于ss段寄存器加载不可写的段描述符的检查。首先是第一个,我们构造的代码段其已经满足是只执行代码段,即不可读,我们直接装载即可,源代码如下所示

[bits 32]
PROTECTION_MODE_START:
    mov    ax, 0000000000001_0_00b
    mov    es, ax

 

  结果如图所示

 

 

  可以看到,如果是不可读的内存段的段描述符,确实不能被加载入es段寄存器。下面我们检测第二个规则。同样用不可写的代码段段描述进行测试,源代码如下所示

[bits 32]
PROTECTION_MODE_START:
    mov    ax, 0000000000001_0_00b
    mov    ss, ax

 

  结果如图所示


代码段和数据段的保护

  实际上,对于代码段和数据段来说,除了上面的检查外,cpu每访问一个地址,都要确认该地址不能超过其所在的内存段的范围。也就是段描述符中段机制和段界限共同描述的范围。这里我们假设其都是默认向上扩展的(主要针对的数据段)。之前已经分析过了,实际段界限的值为

(段描述符中段界限 +) ×(段界限的力度大小:4KB/1B)-1

 

  由于我们实现的GDT中的段描述符基本上其段界限粒度都是4KB的,因此我们可以将上面的公式进行展开,如下所示

实际段界限大小 = 段描述符中界限 * 0x1000 + 0xfff

 

  因此,cpu会在检查的时候判断相关数据(纯数据/代码)最终是否超过了段的边界,也就是检查如下等式

偏移地址+数据长度/指令长度 - 1 <= 实际段界限大小

 

  如果不满足上面的检查,则CPU就会直接抛出异常,导致程序执行错误。我们这里就简单的通过对于前面实现的GDT中的显存段(可以相当于数据段)进行验证,我们前面设置的段描述符的段边界是0x7,转换为对应的实际段界限也就是

0x7 * 0x1000 + 0xfff = 0x7fff

那么我们就访问该段的偏移地址为0x7fff,数据长度为2的数据即可,源代码如下所示

[bits 32]
PROTECTION_MODE_START:
    mov    ax, GDT_SECT_VIDEO
    mov    es, ax
    
    mov byte    al, [es:0x7fff]
    mov word    ax, [es:0x7fff]

 

  执行结果如图所示

 

  可以看到,es对应的段描述符的实际段边界大小是0x7fff,当我们访问es:0x7fff的一字节数据时,程序仍然正常执行;当我们访问es:0x7fff的2字节数据时,cpu检测到了不符合规则,则直接抛出异常。


栈段的保护

  这次我们分析的是以向下扩展的数据段作为栈段(前面的实验我们是以普通的向上扩展的数据段作为栈段,其保护和普通的数据段没有什么区别),用来了解清楚段描述符中各个属性的具体作用。这里我首先想说明清楚一个问题,段描述符中向上扩展/向下扩展的含义。书上是这样讲的

  1.  对于向上扩展的段,实际的段界限是段内可以访问的最后一字节;

  2.  对于向下扩展的段,实际的段界限是段内不可以访问的第一个字节。

  老实说,这里我是花费了很长时间才弄明白的,这里我直接更直观的给出数据和实验结果,方便大家理解的更透彻。

 

 

   这是向上扩展的段描述符对应的可用内存,这个看起来还是比较好理解的,唯一需要注意的是段最大大小,那个是根据段描述符中段界限取最大值和段界限的粒度大小共同得出来的。下面则是向下扩展的段描述符,示意图如下所示

 

   也就是实际上段界限将理论上段的最大值的内存空间一份为二,其中向上扩展使用的是靠基址部分的;向下扩展是另外一部分(由于是32位地址线,如果选择段界限粒度为4K,我认为会地址回绕,不过没有实验)。对于向上扩展的数据段,我们实际上已经在这篇博客的前半部分进行了验证;而对于向下扩展的数据段,我们讲显存段的TYPE中E置为1,然后测试代码如下所示

[bits 32]
PROTECTION_MODE_START:
    mov    ax, GDT_SECT_VIDEO
    mov    es, ax
    
    mov byte    bl, [es:0x8000]
    mov byte    bl, [es:0x7fff]

 

  结果如图所示

 

   可以看到,可以正常访问地址为es:0x8000处的内存,但是无法访问地址为es:0x7fff处的内存。既然我们明白了这个道理,实际上对于向下扩展的栈段就十分容易进行检查了。下面我们使用的仅仅都是段内偏移地址,也就是整个可访问的栈的空间为

[limit + 1, 0xFFFFFFFF/0xFFFFF]

  只需要确保栈上所有数据相对基址的偏移处于这个范围内即可(需要边界的时候还需要考虑数据本身的大小,而非仅仅考虑数据起始地址)

posted @ 2020-09-19 14:30  hawkJW  阅读(421)  评论(0编辑  收藏  举报