本文为<x86汇编语言:从实模式到保护模式> 第12章笔记
别名技术
- 我们都已经知道, 在保护模式下, 代码段是不可写入的. 所谓不可写入, 并非是说改变了内存的物理性质, 使得内存写不进去, 而是说, 通过该段的描述符来访问这个区域时, 处理器不允许向里面写入数据或者更改数据. 但是, 很多时候又需要修改代码段, 如调试时加入断点指令int3. 不管怎么样, 如果需要访问代码段内的数据, 只能重新为该段安装一个新的描述符, 并将其定义为可读可写的数据段. 这样, 需要修改代码段内的数据时, 可以通过这个新的描述符来进行. 像这样, 当两个以上的描述符都描述和指向同一个段时, 把另外的描述符称为别名(alias). 别名技术并非仅用于读写代码代码段, 如果两个程序想共享同一个内存区域, 可以分别为每个程序都创建一个描述符, 而且它们都指向同一个内存段, 这也是别名应用的例子.
修改段寄存器时的保护
- 对段寄存器进行修改时, 处理器在变更段寄存器以及隐藏的描述符高速缓存器的内容时, 要检查其代入值的合法性. 修改段寄存器时, 处理器把指令中的选择子传送到段寄存器的选择器部分, 但是, 处理器的固件在完成传送之前, 要确认选择子是正确的, 并且该选择子选择的描述符也是正确的.
- 如果选择子的TI = 0, 故描述符在GDT中. GDT的基地址和界限在GDTR中, 描述符在内存中的地址, 使用索引号乘以8, 再和描述符表的线性基地址相加得到的, 而这个地址必须在描述符表的地址范围内, 换句话说, 索引号乘以8得到的数值, 必须位于描述符表的边界范围之内, 也就是索引号 * 8 + 7 小于等于边界. 如果检查到指定的段描述符, 其位置超过表的边界, 处理器中止处理, 产生异常中断13, 同时段寄存器中的原值不变.
- 通过了上述检查, 从表中取得描述符后, 紧接着还要对描述符的类别进行确认. 举例来说, 若描述符是只执行的代码段, 则不允许加载到除CS之外的其他段寄存器中. 首先, 描述符的类别字段必须是有效的, 0000是无效值的一个例子. 然后, 检查描述符的类别是否和段寄存器的用途匹配, 规则如下图 最后, 除了按上图进行段的类别检查外, 还要检查描述符中P位, 如果P = 0, 表明描述符已被定义, 但该段实际上并不存在于物理内存中. 此时, 处理器中止处理, 引发异常中断11. 一般来说, 应当定义一个中断处理程序, 把该描述符所对应的段从硬盘等外部存储器调入内存, 然后置P位. 中断返回后, 处理器将再次尝试刚才的操作. 如果P = 1, 则处理器将描述符加载到段寄存器的描述符高速缓存器, 同时置A位(仅限于当前讨论的存储器的段描述符).
- 如上图所示, 可读的代码段类似于ROM. 可以用段超越前缀CS:来读其中的内容, 也可以将它的描述符选择子加载到DS, ES, FS, GS来做为数据段访问, 代码段在任何时候都是不可写的.
- 一旦上述规则全部验证通过, 处理器就将选择子加载到寄存器的选择器. 显然, 只有可以写入的数据段才能加载到SS的选择器, CS寄存器只允许加载代码段描述符. 另外, 对于DS, ES和GS的选择器, 可以向其加载数值为0的选择子, 即 xor eax, eax mov ds, eax. 尽管在加载时不会有任何问题, 但在真正要用来访问内存时, 会导致一个异常中断. 这是一个特殊的设计, 处理器用它来保证系统安全, 这在后面会讲到. 不过对于CS和SS的选择器来说, 不允许向其传送为0的选择子.
地址变换时的保护
- 指令开始执行之前, 处理器必须检验其存放地址的有效性, 以防止执行超出允许范围之外的指令. 每个代码段都有自己的段界限, 位于其描述符中. 实际使用的段界限, 其数值和粒度(G)位有关, 如果G = 0, 实际使用的段界限就是描述符中记载的段界限, 如果G = 1, 则实际使用的段界限为 描述符的段界限值 * 0x1000 + 0xfff( (描述符中的段界限值 + 1) * 0x1000 - 1 ). 代码段是向上(高地址方向)扩展的, 因此, 实际使用的段界限就是当前段内最后一个允许访问的偏移地址. 当处理器在该段内取指令执行时, 偏移地址由EIP提供, 指令很有可能是跨越边界的, 一部分在边界之内, 一部分在边界之外, 或者一条单字节指令正好位于边界上. 因此, 要执行的那条指令, 其长度减1后, 与EIP寄存器的值相加, 结果必须小于等于实际使用的段界限, 否则引发处理器异常, 即: 0 <= (EIP + 指令长度 - 1) <= 实际使用的段界限. 任何指令都不允许, 也不可能向代码段写入数据. 而且, 只有在代码段可读的情况下(由描述符指定), 才能由指令读取其内容.
栈操作时的保护
- 对栈操作的指令一般是push, pop, ret, iret等. 这些指令在代码段中执行, 但实际操作的是栈段. 现在只讨论32位栈段, 即, 其描述符的B位是1的栈段, 处理器在这样的段上执行压栈和出栈操作时, 默认使用ESP寄存器.
- 在栈段中, 实际使用的段界限也和粒度(G)位相关, 如果G = 0, 实际使用的段界限就是描述符中记载的段界限; 如果G = 1, 则实际使用的段界限为描述符中的段界限值 * 0x1000 + 0xfff. 栈段是向下扩展的, 每当网栈中压入数据时, ESP的内容要减去操作数的长度. 所以, 和向高低址方向扩展的段相比, 非常重要的一点就是, 实际使用的段界限就是段内不允许访问的最低端偏移地址. 至于最高端的地址, 则没有限制, 最大可以是0xffffffff. 也就是说, 在进行栈操作时, 必须符合以下规则: 实际使用的段界限 + 1 <= (ESP的内容 - 操作数的长度) <= 0xffffffff.
- 举例来说, 现在栈段的粒度是字节(G = 0), 描述符中的段界限是0x07a00, 此时, 实际使用的段界限也是0x07a00. 假设现在ESP的内容是0x00007a04, 那么, 执行push edx后, 要压入一个双字, 故处理器在向栈中写入数据之前, 先将ESP的内容减去4, 得到0x7a00, 这就是ESP寄存器在进行压栈操作的新值. 因为该值小于实际使用的段界限0x7a00加1(0x7a01), 因此不允许执行该操作. 但是如果执行 push ax指令, 因为要压入一个字(2字节), 故实际执行压栈操作时, ESP的内容是0x7a04 - 2 = 0x7a02, 结果大于实际使用的段界限加1, 允许操作.
数据访问时的保护
- 这里所说的数据段, 特指向上扩展的数据段, 有别于栈和向下扩展的数据段. 因为是向上扩展的, 所以代码段的检查规则同样适用于数据段, 不同之处仅仅在于, 对于取指令来说, 是否越界取决于指令的长度; 而对于数据段来说, 则取决于操作数的尺寸.
- 举例来说, mov [0x2000], edx, 这条指令中给出了内存单元的有效地址EA(0x2000), 也给出了操作数的大小(4). 当处理器访问数据段时, 要依据以下规则进行检查:0 <= (EA + 操作数大小 - 1) <= 实际使用的段界限. 在任何时候, 段界限之外的访问企图都会被阻止, 并引发处理器异常中断.
- 在32位处理器上, 尽管段界限的检查总在进行着, 但如果段界限具有最大值, 则对任何内存地址的访问都将不会违例. 如段基地址为0x00000000, 段界限是0xfffff, 粒度为4KB, 因此, 实际使用的段界限是0xfffff * 0x1000 + 0xfff = 0xffffffff. 在这样的段内, 访问任何一个内存单元都是允许的, 针对段界限的检查都会获得通过. 在32位模式下, 处理器使用32位的段基地址加上32位的偏移量, 共同形成32位的物理地址来访问内存. 段基地址由段描述符指定, 而偏移量由指令直接或间接给出. 很显然, 在段最大的时候, 可以自由访问4GB空间内的任何一个单元. 如此时DS指向0到4GB的内存空间(段基地址为0, 段界限为0xfffff, G位 = 1), 可以用mov指令自由访问4GB的空间. 如 mov [0x00401000], 0(没有段超越前缀, 默认DS)
时间: 2024-10-21 07:51:45