存储器的保护

xiaoeryu Lv5

01. MOV DS, AX和 MOV DS, EAX

这两条指令计算GDT的逻辑段地址,使用64位除法指令: eax(商)保存段地址, edx(余数)保存偏移
div r/m32

mov ds, ax在16位操作尺寸下机器码是8ED8, 在32位操作尺寸下应该是668ED8, 但是Intel的官方文档对这种指令做了优化:

但是有些编译器在编译这条指令的时候仍然会加上66前缀所以官方文档建议使用mov ds, eax, 不过在NASM编译器下编译的结果不管是mov ds, eax还是mov ds, ax在16还是32操作尺寸下都不会有前缀66.
编译之后查看lst列表文件:
使用16位操作尺寸时:

使用32位操作尺寸时:

可以看到NASM编译器将指令译码成了8ED8.
处理器不允许在任何时候使用索引字段为0的选择子去访问0号描述符, 任何时候的访问都会引发异常中断

  • 此节程序设置了GDT逻辑地址, 安装了描述符, 加载初始化了GDTR, 打开A20地址线, 清除中断, 设置CR0寄存器的PE位进入保护模式, jmpf进入保护模式执行.

02. 修改段寄存器的保护

本节主要内容:介绍保护模式下修改段寄存器时的保护机制

保护模式下对段寄存器的修改要分为两步:
1. 将一个描述符选择子带入段选择器此时要检查带入值的合法性
2. 用选择子选择一个描述符并传送到段描述符高速缓存器, 此时要检查描述符的完整性和正确性

在上一节进入到保护模式之后, 执行了jmpf, 此时给出的段选择子的TI位都是0, 表示要选择的描述符都在GDT中. 同时也要执行两步检查来确认这一过程的正确

  1. 将一个描述符选择子带入段选择器此时要检查带入值的合法性

    1. 处理器会检查访问的描述符的边界, 如果超过了边界条件就会产生一个异常中断13, 同时寄存器保持原来的值不变.
  2. 用选择子选择一个描述符并传送到段描述符高速缓存器, 此时要检查描述符的完整性和正确性

    1. 确定描述符的类型, 比如描述符S=1, X=1, 表示此描述符代表一个代码段, 只能加载到段寄存器CS. 首先表示描述符类型TYPE的四个字段必须是有效的(X E W A), 接着按下表检查描述符的类型与段寄存器是否匹配
  3. 检查描述符中的P位是否为1, 若为0表示虽然描述符已经定义, 但是它对应的段并不存在物理内存中, 此时处理器终止处理并引发11号中断. 一般来说需要定义一个中断处理程序接管11号中断, 在中断处理的过程中把该描述符所对应的段从外部硬盘等存储器中调入内存, 然后将P位置1. 这种类型的中断返回时并不是返回到下一条指令二十返回到引起中断的那条指令, 这次就可以重新加载描述符到段寄存器了. 如果P位=1处理器将描述符加载到段描述符对应的描述符高速缓存器中, 同时将A位置1.
    代码段在R=1的时候类似于只读存储器ROM可以使用段超越前缀CS来读取其中的内容也可以将它们的描述符选择子加载到DS ES FS GS来作为数据段访问, 但是代码段在任何时候都是不可写的.

  4. 一旦上述验证全部通过, 处理器就将段选择子加载到段寄存器段选择器中.

另外还有一些注意事项:

03. 代码段执行时的保护

进入保护模式之后转移指令jmp 0x0010:flush会修改段寄存器CS, 会进行一系列检查工作:

  • 首先选择子0x0010指定的描述符不能超过描述符表的边界;
  • 其次指定的描述符必须是代码段描述符, 相关的信息必须是完整且合法的;
  • 还要检查偏移量flush是否超出了当前段的界限. 因为是刚进入保护模式, CS的描述符高速缓存器中的内容还是之前的值, 段界限是0XFFFF.
    一旦通过了检查, 就将选择子0x0010带入CS的段选择器, 并用描述符填充描述符高速缓存器, 接着用偏移量flush修改指令指针寄存器EIP, 处理器立即转入目标位置处执行.
    因为这一条指令刷新了段寄存器CS, 导致处理器到一个新的代码段执行.
    这个描述符指定了D位是1表示处理器的默认操作尺寸是32位的, 因为这个描述符所指定的段在jmp指令之前是在16位操作尺寸下运行的, 之后是在32位操作尺寸下运行的. 在保护模式下一旦相应的描述符被加载到CS描述符高速缓存器中, 则处理器取指令和执行指令的时候就不在访问描述符表, 二十直接使用CS描述符高速缓存器的内容, 从中取得线性基地址同指令指针寄存器EIP的值相加形成32位线性基地址, 从内存中取得下一条指令.

在执行下一条指令之前, 处理器也要检查指令的地址是否有效, 以防止执行超出允许范围之外的指令.

每个段都有其段界限, 位于其描述符中实际使用的段界限其粒度取决于描述符的G位, 实际使用的段界限也要满足于0 < EIP+指令长度-1 >实际使用的段界限.

04. 用向上扩展的段作为栈段

选择01号描述符

查看01号段描述符

  • 基地址: 为0x00000000
  • S位: 为1
  • X位: 为0, 表示数据段
  • E位: 为0, 表示向上扩展
  • 段界限: 为0xFFFFF
  • G位: 为1, 表示段的粒度为4K字节, 段界限是以4K字节为单位

这个数据段所占用的空间与之前的代码段重叠了, 我们知道代码段是不允许写入的, 但是不允许写入指的是不允许通过代码段的描述符来写入代码段, 是可以通过这个重叠的4G字节的数据段来写入代码段.

设置栈段:

二进制11是第四个描述符

  • X位: 为0, 表示为数据段
  • B位(因为是数据段所以D/B位是B位): 为1表示使用ESP(为0则使用SP)
  • E位: 为0, 表示是向上扩展的段
  • 基地址: 为0x00006C00
  • 段界限: 为0x007FF
  • G位: 为0, 表示段界限的粒度以字节为单位
这个栈段是数据段, 共2K字节

栈的扩展方向推进方向不同, 段扩展方向不是数据的读写方向, 而是用来定义偏移量的范围, 界限检查.
段的扩展方向决定了处理器如何对段的访问进行检查

因为这是数据段, 所以是B位, 且B位的值为1, 表示使用ESP当作栈的指针, 所以要设置ESP的初始值为段的大小(段界限0x7FF + 1 = 0x800 = 2048).

05. 向上扩展的段作为栈段时的保护

上一节中使用的栈段为2K字节:

且使用ESP当作栈指针寄存器来进行隐式的栈操作指令, 如:

这些指令操作时, 会对栈进行压栈, 出栈.

使用下列两条代码演示栈的检查:

在压入时对偏移量进行检查:

第一条指令压栈后的状态, 和检查过程:

正好符合使用界限的要求, 若将ESP的初始值设置为大于0x800比如0x802, 再进行压栈指令操作时就会将数据压入到边界之外, 会导致处理器产生中断.

06. 访问普通数据段时的保护

将上一节压入的数据弹出:

当前栈的布局:

首先需要检查出栈的数据是否超出栈的边界:

检查通过之后:
1. 使用SS的基地址0x00006C00加上当前ESP中的偏移值0x07F8得到有效地址0x00073F8, 从这个地址处出去四个字节数据.
2. 之后使用默认的数据段DS段地址0x00000000加上指令中给出的偏移量0x0B800得到有效地址为0x000B8000, 将栈中弹出的数据写入此处.

在写入之前, 处理器要检查是否会超出段界限之外:

写入之后数据段DS的状态:

3. 出栈之后, 处理器将ESP的值加4变为0x07FC

07. 内存线性地址的回绕特性

段的逻辑地址为0x00000000, 加上右边的偏移量, 得到逻辑地址

还可以使用下列方式表示偏移量:

08. 用向下扩展的段作为栈段

之前使用向上扩展的段作为栈段, 大小为2K字节.

描述符:

  • S位: 为1, 表示为寄存器的段描述符
  • X位: 为0, 表示数据段描述符, 为1表示是代码段描述符
  • E位: 为0, 表示是向上扩展的段, 为1表示是向下扩展的段
    向下扩展的数据段实际允许使用的偏移量范围 = 实际允许使用的段界限 + 1开始的, 段内偏移量的最大值为0xFFFF0xFFFFFFFF.

对于向下扩展的段:

程序中使用的第二个栈段:

程序中使用的第二个栈段描述符:

  • S位: 为1, 表示存储器的段描述符
  • X位: 为0, 表示数据段
  • 段基地址: 0x00007C00
  • E位: 为1, 表示向下扩展
  • W位: 为1, 表示可读可写
  • 段界限: 0xFFFFFE
  • G位: 为1, 表示粒度以4K字节为单位
  • B位: 为1, 表示偏移量理论最大值是0xFFFFFFFF, 而且使用ESP位栈指针

这个栈段的状态如下图:

ESP的值要设置为栈的最大界限值 + 1: 0xFFFFFFFF +1 = 0x00000000.

09. 向下扩展的段作为栈段时的保护

上一节中设置的栈:

使用下列指令执行压栈操作:

在执行压栈指令时, 需要对压入的数据进行检查, 以确定不会超出段的界限:

具体为处理器先执行ESP - 4 = 0xFFFFFFFC, 0xFFFFFFFC > 实际使用的段界限0xFFFFEFFF, 符合上述条件执行压栈操作.

接下来处理器使用段的基地址0x00007C00 + 0xFFFFFFFC = 0x00007BFC, 从这里写入数据.

压栈后执行出栈操作

出栈:

10. 通过别名来实现段的共用和共享

在保护模式下不能使用CS来修改内存, 但是可以设置一个代码段的别名描述符, 将其X位设置为0表示数据段, 这样就可以通过ES来访问字符串所在的内存位置, 以达到修改内存的操作.

11. 冒泡排序的基本原理

12. 32位操作尺寸下的LOOP指令

如果CS高速缓存器中的D位为0表示使用16位操作尺寸, loop指令使用cx计数, 如果D=1那么表示使用32位操作尺寸, loop使用ecx计数.
冒泡排序循环:@@1处

冒泡排序内循环: @@2处

其中外循环比较次数: 可以看出内循环比较次数和外循环的ECX相同

冒泡内循环比较过程:

13. 数据交换指令XCHG

XCHG指令: 操作数不能同时为内存地址

  • 标题: 存储器的保护
  • 作者: xiaoeryu
  • 创建于 : 2022-12-11 22:42:00
  • 更新于 : 2023-10-03 12:59:36
  • 链接: https://github.com/xiaoeryu/2022/12/11/24-存储器的保护/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论