386对存储器的保护与栈段初始化

来源:互联网 发布:1688一键传淘宝 编辑:程序博客网 时间:2024/05/16 16:01
386对存储器的保护与栈段初始化
    1. 更换段寄存器时要注意段选择子不能指向GDT以外的位置
    2. 更换段选择器时要注意段的属性要和段寄存器相符,比如数据段不得由CS指向,SS只能指向数据段,且该数据段必须可写。
    3. 每利用"偏移地址寄存器"进行寻址时,比如BX,IP,SS,CPU都会检测偏移地址是否超出该段得合法地址范围,如果超出,会引发中断。至于段的合法地址如何计算,见4。
    4. 如何计算段的合法地址范围,这是个容易让人混淆的问题
        1)段的大小由段描述符中的段界限和G(Granularity)共同决定
            如果G=0,段大小 = (段界限+1) byte
            如果G=1,段大小 = (段界限+1) * 4KB
            因为段界限最大为0xFFFFF,故段最大为1MB(G=0时)或者4GB(G=1时)
        2)段向高地址拓展,比如代码段和常见的数据段,则偏移地址从0开始,因此
                  G = 0:
                    最大偏移量 = 段大小-1 = (段界限+1) -1 = 段界限
                  G = 1:
                    最大偏移量 = (段界限 + 1) * 4KB - 1 = (段界限 + 1) * 0x1000 - 1 = 段界限 * 0x1000 + 0xfff
          向高地址拓展的段的合法地址范围是:
                  [基地址, 基地址 + 最大偏移量],"["与"]"表示包含边界
          其实,当G=0时,段界限是一个容易理解的概念,因为此时,段界限就是最大偏移量,比如段界限为511表示该段最大偏移为511,再考虑到偏移地址从0开始,因此该段共512字节。但是当G=1时,事情就要麻烦的多。此时可以把段界限看作对4KB的"块"进行计数。段界限为0表示0号块,段界限为0xFFFFF,表示0xFFFFF号块。
        3)保护模式下栈的使用非常容易令人迷惑。栈一般使用向低地址拓展的段实现。拓展方向会影响CPU的段界限检查。在向高地址方向拓展的段内,通过界限检查意味着:
                                            偏移 <= 最大偏移量
          而在向低地址方向拓展的段内,通过界限检查意味着:
                                            偏移 > 最大偏移量 (偏移无上限)
          一个例子:栈段的基地址 = 0x7c00,界限 = 0xffffe, G = 1:
                      最大偏移量 = 段界限 * 0x1000 + 0xfff = 0xffff efff
                      最小的合法偏移 = 最大偏移量 + 1 = 0xffff f000
                      最大的合法偏移 = 0xffff ffff
                      因此,栈段的合法地址范围:上限  0x7c00 + 0xffff ffff = 0x0000 7BFF
                                                下限  0x7c00 + 0xffff f000 = 0x0000 6C00
          每次执行段操作,比如push,call,ret等前,CPU都会根据ESP和目标操作数的大小判断对栈端的访问是否会超出合法地址范围,若超出,则触发中断。
       4)我们可以给同一个地址范围注册两个段。常见的情况是一个段是只可运行的代码段,而另一个则是可写数据段,后者一般称作前者的“别名”。利用别名,我们可以修改代码段的代码。好邪恶。



    根据上面所说的栈段合法地址计算方法(4(3)),可以得到一个声明段的方法,不过在此之前,先介绍另一种声明段的方法并指出其弊病。
    譬如为512字节的引导程序声明一个4KB大小的栈,栈的范围是[0x6c00,0x7bff],刚好在0x7c00之前。
    第一种方法是设置基地址为0x0000,G=0,段界限是0x6c00,初始化ESP为0x7c00。段界限为0x6c00意味着ESP > 0x6c00,又因为ESP初始化为了0x7c00,因此我们看似得到了一个4KB大小的堆栈,从0x7c00到0x6c00。当然,这样声明的一个问题是该段不包含地址边界0x6c00,但这不是主要问题,因为我们只需要将段界限设置为0x5bff即可。真正的问题在于该段的范围大大超出了我们的预想。该段真正的范围是:0x6c00 ~ 0xffffffff。这个段可以access到几乎整个4GB内存——这是存储器保护机制不希望看到的。
    第二种方法繁琐一些,但的确有效。繁琐是因为它利用了地址回绕,有效是因为它可以准确无误的给出栈段的上下界。当确定了基地址之后,我们只需确定出ESP的上下界即可确定栈段的上下界:
                                   stack_segment_base + max_ESP = stack_segment_max_address
                                   stack_segment_base + min_ESP = stack_segment_min_address
比如我们希望得到栈[0x6c00,0x7bff],这要求stack_segment_base + 0xffff ffff = 0x7bff,从而得到stack_segment_base = 0x7c00。另一方面,0x7c00 + min_ESP = 0x6c00,从而得到min_ESP = 0xffff f000。这要求最大偏移量是0xffff efff。如果G=0,那么段界限就是最大偏移量,也就是0xffff efff,但段界限长20个bit,因此必须要求G=1。G=1时,段界限为0xffffe。段描述符设置好后,将ESP初始化为0即可完成对段的声明。上述过程很繁琐,但从地址回绕的角度不难得到直观的理解。试想我们将栈段基地址设置为0x7c00,ESP初始化为0,那么push操作会让ESP回绕到高地址端(0xFFFFFFFF),从而让0x7c00+ESP回绕到0x7c00的“前面”(0x7bff)。之后的push操作会让ESP从高端地址向低端地址运动,相应的0x7c00+ESP会从0x7c00向低端地址运动,直到触碰到ESP的下界0x0xffff f000(含),此时0x7c00 + ESP = 0x0000 6c00(含)。在引入下述对段界限的直观解释后,上面的解释会变得更加明了:
    当G=1时,我们从基地址base开始将地址空间分割成4KB大小的块,然后编号0x0,0x1,0x2...0xfffff(已经回绕到了base-1),那么对于向高地址扩展的段,段界限为n意味着使用内存块0x0-n,对于向低地址扩展的段,段界限为n意味使用内存块n+1-0xfffff。
    当G=0时...(1MB空间的分割)
因此基地址0x7c00,段界限0xffffe,向低地址方向扩展意味着该段的实际范围是编号为0xfffff的块,也就是最后一个块,也就是从0x7c00(不含)向前数4KB。此时ESP=0指向基地址0x7c00。
0 0