您的位置:首页 > 其它

存储器的保护

2016-07-24 20:47 232 查看

我们都已经知道, 在保护模式下, 代码段是不可写入的. 所谓不可写入, 并非是说改变了内存的物理性质, 使得内存写不进去, 而是说, 通过该段的描述符来访问这个区域时, 处理器不允许向里面写入数据或者更改数据. 但是, 很多时候又需要修改代码段, 如调试时加入断点指令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)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: