如何进入保护模式

来源:互联网 发布:1688一键传淘宝 编辑:程序博客网 时间:2024/06/10 01:31
如何进入保护模式
    这是个复杂的过程,其原因在于保护模式本身是复杂的。
    相对于8086来说,386增加了很多新寄存器,用于支持多任务操作系统,或者说让CPU能工作在保护模式下。CR0就是其中之一。CR指Control Register,控制寄存器。CR0是CR中的一个。CR的最后一个bit是PE位,Protected-mode Enable,保护模式使能位。386开机后工作在实模式,可以通过16位指令置CR0的最后一个bit为1,从此CPU进入保护模式。
    set PE操作虽然能让CPU进入保护模式,但这并不意味着进入保护模式的CPU可以正常工作。这是因为相对于实模式下的CPU,保护模式下的CPU需要依赖一些特殊的数据结构才能正常工作,除此之外,很多琐碎的问题也需要考虑在内。
    首先,为了给存储器内的段提供独立性,386引入了一个数据结构GDT,Global Descriptor Table。386有32位的寄存器,32根地址线,不在依靠分段机制访问“超过寄存器长度的存储器地址(指段地址:偏移的寻址方式)”,但是出于兼容性与内存管理等方面的考虑,段的概念依然被保留在了386内。此时,段机制的意义只在与将存储器分割成了更小的部分。为了让这些段具有独立性,Intel引入了GDT,保存各个段在存储器中的位置与和段隔离有关的信息。该表可以很大,保存在RAM中,而386通过GDTR(GDT Register),一个32(base address) + 16(boundary)= 48位的寄存器,定位GDT的位置。从寄存上看,GDT可以存在于4GB RAM的任何位置,但实模式下,386只能寻址1MB。因此GDT往往在RAM的第一个1MB内。GDT是一个数据结构,存储为保证段独立性而必须的信息。换句话说,GDT保存了对段的描述,而且是一个非常细致的描述。保护模式下,CPU内的段寄存器不再保存段地址,而是保存索引号(index),指向GDT中的描述信息。“段寄存器改变”对于CPU来说,隐含有操作“将段寄存器指向的段描述信息读入该段寄存器对应的描述符高速缓存”。从此,我们可以根据描述信息访问内存中的段。GDT表中的一个条目长64个bit,含有以下信息:
                   1. 32bit基地址与20bit界限,用以标明该段在内存中的位置和大小。说到这儿,我想起了...见ps2
                   2. G,Granularity,粒度,也就是段界限的单位,有byte和4KB两种,前者对应最大1MB的段,后者对应最大4GB的段。
                   3. 描述信息
                        S,Descriptor Type,描述符类型;
                        DPL,Descriptor Privilege Level,描述符特权级;
                        P,Segment Present,段存在位;
                        D/B,Default Operation Size/Default Stack Pointer Size,默认操作数大小/默认堆栈指针大小;  
                        L,64-bit Code Segment,保留给64-bit 处理器使用。
                        TYPE,用以描述段属性。代码段和数据段共有的属性是可运行性(X,eXcutable)和最近已访问性(A,Access),对于代码段,X一定是1,而数据段的X一定为0;A在该段被访问时会被set,向操作系统提                             供内存管理的依据。数据段另有两个属性,扩展方向(E,ExtendDirection?)和可写性(W,Writable)。E和W略。代码段另有两个属性,特权已从(C,Conforming)和(R,Readable)。C和R略。
                        AVL(Avaible,随便使用的一个bit)
由上可知,相对于8086的简单的段寄存器,GDT给出了段的大小和位置,以及一套相当细致的描述信息。这些描述信息有两个作用:一是提供段和段之间的隔离;二是为操作系统进行内存管理提供依据。在进入保护模式之前,386必须初始化GDT,让GDTR指向GDT。
    第二,A20问题。A20问题来自于386对8086程序的支持。8086有20根地址线:A0-A19。很多8086程序利用了“地址回环”。所谓“地址回环”指当地址超过20根地址线所能表达的范围,即0xFFFFF,地址将回环到0x00000。因此,如果16位段地址很“靠后”,比如段地址为0xFFFF时,该段其实要“回环”到内存的头部。但是这些利用了“地址回环”的程序不能被386实模式兼容,因为386的地址线长达32位。如果出现了超过0xFFFFF地址,比如0x1 00000,这些地址超过20位的部分会被保留(在8086里会被舍弃)用来去寻址RAM——这和编写那些程序的程序员所期待的“地址回环”不同。初期的解决办法是利用外部芯片将A20强制清0。当然,为了让地址彻底“坍缩”成20位地址,似乎要将A21-A31全部清0,但是貌似不会出现那么高的地址,毕竟产生A21为1的地址,意味着寻址超过0x20 0000——即使段地址最大,取0xFFFF,该段的最高地址也不会超过0xFFFF*16+0xFFFF = 0x16 FFE9。因此将A20清0就够了。后期的解决方法是由处理器提供一个控制引脚A20M#,M指Mask,低电平有效。也就是低电平会让A20始终为0。ICH芯片的92端口的bit1可以用来控制A20M#,0对应低电平,也就是A20始终为0,1对应高电平,也就是启用A20。进入保护模式后,如果A20依然被屏蔽,会导致寻址“跳跃”。因此在进入保护模式前,必须通过92端口启用A20。
    第三,CLI。进入保护模式后,保护模式下的中断机制尚未建立而实模式的终端机制已经失效,因此进入PE位set之前,必须cli。
    第四,刷新段寄存器,清空流水线。set PE后,CPU进入保护模式。在CPU看来,段寄存器里的内容并没有变化,但是含义却从“段地址”变成了“段选择子”——这是个很严重的错误。因此,set PE后,应该立刻将段寄存器更新,而最紧迫的则是更新CS。因为CS会参与决定set PE后马上执行的指令是什么。另外,由于16位模式的386和32位模式的386对同样的机器码会有不同的理解(即译码结果不同,),因此流水线必须清空。解决上述两个问题的方法就是set PE后,马上执行一次远转移的jmp。jmp会更新CS,顺带更新CS的描述符高速缓存,同时清空流水线。这条指令应该是这个样子:jmp index:offset,其中index作为段选择子指向本代码段。表面上看,因为这条指令在set PE之后,所以应该在32 bits模式下编译,进而得到如下的代码结构:
           [bits 16]
           ....;LGDT,A20,CLI,SET PE
           [bits 32]
           jmp index:offset
           ...
但上述代码会发生错误。在[bits 32]关键字下,jmp index:offset被编译成EA |32bits of offset |16bits of index,而在[bits 16]关键字下,jmp index:offset被编译成EA |16bits of offset |16bits of index。前者会导致刚刚进入保护模式的CPU发生错误。首先,由于流水线的存在,译码单元以16bit模式译码机器码EA |32bits of offset |16bits of index,从而产生预料之外的jmp指令。CPU会将32bits of offset的前16个bit作为offset赋给IP,后16bit作为index赋给CS——这和预想的完全不同。一个显而易见的纠正方法是采用如下结构的代码:
           [bits 16]
           ....;LGDT,A20,CLI,SET PE
           jmp index:offset           
           [bits 32]
           ...
在[bits 16]关键字下,jmp index:offset被编译成EA |16bits of offset |16bits of index。指令会导致CS被赋值16bits of index,CS的描述符高速缓冲被刷新,而EIP的后16bit后16bit后16bit后16bit被赋值16bits of offset...如果EIP的前16位为0的话,一切都会很顺利,但如果不是呢...妥善的解决办法是使用如下的代码:
           [bits 16]
           ....;LGDT,A20,CLI,SET PE
           jmp dword index:offset           
           [bits 32]
           ...
dword关键字告知编译器"使用32bit offset",从而导致编译结果变成: 66 | EA | 32bits of offset| 16bits of index。0x66前缀将参与译码,提醒CPU将32bits of offset赋给EIP。这儿有些疑问,见PS1。
    终于进入保护模式了。




    PS1: set PE后的第一条指令很“繁琐”。《x86汇编语言-从实模式到保护模式》一书中解释说:处理器刚刚进入保护模式,CS段的段描述符高速缓冲为全0,所以D/B位也为0,进而导致处理器对指令的操作依然采用16bit寄存器和16bit操作数——处理器在处理jmp时,工作在“16位保护模式”。但是,我怀疑这个解释的正确性。事实上,jmp操作的复杂性就在于这条指令"在16位模式下预取,在32位模式下执行"。预取的时候,处理器还没有进入32位模式,因此这条指令其实是在16位模式下进行的指令译码。在执行这条指令的时候,D/B位并没有起作用。我猜测D/B位参与的是流水线前期的工作,包括译码在内。总之,jmp指令执行时,我们必须认为处理器依然运行在16位模式,但是其原因究竟是D/B位在起作用,还是16位模式下对jmp的预取,我认为有待讨论。

    PS2: winXP下用C++写程序,如果使用一个长达1MB的数组,比如char a[1024*1024],程序运行时会发生错误。即使将数组拆分成两个,比如char a[512*1024],b[512*1024],也行不通。经过简单的测试可以发现,可以保证程序正常运行的数组的最大大小是一个接近1MB的值。发生这个事情的原因是否就是GDT条目中用20bit描述段界限呢?
0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 大王卡上网慢怎么办 荣耀10网速慢怎么办 4g手机网卡怎么办 电信3g网速不好怎么办 手机指纹锁打不开了怎么办 随身wifi坏了怎么办 华为手机耗流量怎么办 华为的卡槽坏了怎么办 手机内存小了怎么办 华为平板停止运行怎么办 华为p6一直黑屏怎么办 华为手机root卡重启怎么办 华为手机变砖怎么办 华为手机丢了怎么办? 蓝牙耳机不闪烁怎么办 金立手机打不开怎么办 华为蓝牙不能用怎么办 iphone蓝牙坏了怎么办 苹果蓝牙坏了怎么办 苹果x蓝牙连不上怎么办 手机耳机槽松了怎么办 蓝牙连接不上怎么办 蓝牙开不了机怎么办 蓝牙耳机听不了怎么办 路虎车门打不开怎么办 蓝牙连接声音小怎么办 手机丢在高铁上怎么办 电信卡注销欠费怎么办 手机和卡都丢了怎么办 老干妈打不开盖子怎么办 苹果键盘电池仓打不开怎么办 苏泊尔电压力锅打不开盖子怎么办 honorv9声音太小怎么办 异地手机卡丢了怎么办 中国移动卡丢了怎么办 mate8麦克风坏了怎么办 qq空间被禁赞了怎么办 苹果老耳机模式怎么办 华为手机无声音怎么办 华为手机声音不正常怎么办 带耳机不能说话怎么办