X86 CPU在段式管理下的地址形成机制
以及BIOS初始化过程对这种机制的利用
陈英豪 中科院计算所
2005-7-28
我们对x86 cpu的多少年来的某些刻骨铭心的理解可能是不正确的。这里之所以用了“可能”一词,是因为下面的一些结论是建立在我自己的实验和推测的基础之上的,没有 绝对的把握,但我基本上都是以官方的手册或资料(当然还包括从网上收罗到的一大堆高手们写作的很有价值的但却很难找到出处的非官方资料)作为实验和推测的 根据,所以这些结论也绝不是臆想出来的,并且在很大程度上具有说服力。另外为了帮助理解,我对某些问题的相关基础知识都作了适当的补充。下面切入正题。
一、x86的地址翻译机制(知识准备)
段
:
偏移
段地址翻译机制
线性地址
物理地址
分页
页地址翻译机制
是
否
旁路掉分页机制
虚拟地址
段地址翻译
线性地址
页地址翻译或bypass
物理地址
图1 地址翻译过程
由于历史原因,在存储管理上,80x86系列的所有cpu都支持段式管理方式(分段方式),并且分段是地址翻译的必经阶段(后面提到的分页是可选的)。我 们把段式地址翻译之前所使用的“段基址 : 偏移量”的地址形式,也就是程序员使用的地址形式称为虚拟地址,很显然虚拟地址是二维的。而虚拟地址经过段翻译后得到的地址称为线性地址,这时的线性地址 已经变成平坦的一维的形式了。如果不分页,这个线性地址就是实际的物理内存地址(简称物理地址)了。如果启用分页机制,再由分页地址翻译机制将线性地址译 成物理内存地址。最后形成的物理地址显然也是一维的。整个地址翻译过程可以用下图来表示:
二、关于神秘的A20(知识准备)
上面只提供了一个地址形成机制的理论框架。接下来我们来看看单就实模式而言,由一个虚拟地址得到相应的物理地址的具体过程是怎样的。我们知 道,8086 cpu比较单纯,cpu内部的段寄存器和其它的寄存器宽度都是16位的,只有20根地址线(A0-A19),并且没有什么保护模式和分页一说。它的段翻译 机制使用如下方法将虚拟地址翻译成线性地址:将16位的段寄存器的值左移4个二进制位(也就是乘以16),再加上16位的偏移地址,形成一个20位的线性 地址,由于没有分页机制,所以形成的线性地址也就是实际的物理内存地址了。
但有一个细节,我们不能忽略,那就是8086的20根地址线只能访问1M(220)的地址空间,也就是00000h~FFFFFh。但8086的这 种地址翻译机制却很明显允许我们使用一个段地址是FFFFh偏移地址也是FFFFh的虚拟地址:FFFFh : FFFFh。这个虚拟地址形成的物理地址为FFFF0h + FFFFh = 10FFEFh,但这个地址已经超出了1M(它比1M多64K-16字节),由于地址宽度有限,截掉高位的进位,最后形成的物理地址实际上 是:0FFEFh,这种地址循环的地址翻译方式我们称之为wrap-around。也就是说,程序员访问100000h~10FFEFh之间的内存时,会 被处理器自动wrap-around去访问00000h~0FFEFh的内存空间。这也是后来出现一根特殊的A20地址线的历史根源。
80286出现以后,地址线增加到24根,可以访问到最大16M(224)的物理内存空间,并且有了保护模式,情况变得复杂起来。Intel在设计 80286时提出的目标是,在实模式下,系统所表现的行为应该和8086/8088所表现的完全一样,也就是说,在实模式下,80286以及后续系列,应 该和8086/8088完全兼容。但最终,80286芯片却存在一个BUG:如果程序员访问100000H-10FFEFH之间的内存,系统将实际访问这 块内存,而不是象过去一样重新从0开始。
为了解决上述问题,IBM使用键盘控制器上剩余的一些输出线来管理第21根地址线(从0开始数是第20根,被称为A20 Gate)。如果A20 Gate被打开,则当程序员给出100000H-10FFEFH之间的地址的时候,系统将真正访问这块内存区域;如果A20 Gate被禁止,则当程序员给出100000H-10FFEFH之间的地址的时候,系统仍然使用8086/8088的方式。绝大多数IBM PC兼容机默认的A20 Gate是被禁止的。由于在当时没有更好的方法来解决这个问题,所以IBM使用了键盘控制器来操作A20 Gate,但这只是一种黑客行为,毕竟A20 Gate和键盘操作没有任何关系。在许多新型PC上存在着一种通过芯片来直接控制A20 Gate的BIOS功能。从性能上,这种方法比通过键盘控制器来控制A20 Gate要稍微高一点。
我们也不难发现,在实模式下即使打开了A20,也只能访问到最大1024K + 64K – 16Byte的地址空间。这是由其地址翻译机制的先天因素所决定的。这正好又给我们展示出一个值得关注的地方。那就是:在实模式下,由它的物理地址形成机 制(段地址左移4位加偏移)最多只需要21根地址线(原来的20根地址线再加上A20)就能解决寻址问题,但保护模式下可以访问的空间要比这大得多,肯定 还需要使用A20以上的地址线,但如果此时A20却关闭了,那会带来什么样的后果呢?也就是说在进入保护模式时,不打开A20,会出现什么样的情况呢?
首先我们必须明白关闭A20和打开A20是什么样一个意思。所谓关闭就是将A20恒置0,也就是通过地址翻译机制得到的物理地址对该位不会有影响; 所谓打开就是允许按实际的地址翻译机制得到的物理地址去设置该位。对于关闭A20的情况,我们只要仔细分析一下地址字,将其第21位恒置0,于是可以得到 地址线宽度所决定的地址空间范围内任意的奇数兆段的地址,如 1M(00000h~FFFFFh),3M(200000h~2FFFFFh),5M(400000h~4FFFFFh),但却得不到偶数兆段的地址。所 以我们进保护模式之前都要习惯性的开启A20,A20的相关电路虽然不是在CPU内部,但与CPU关系密切,所以就多说了几句。
三、x86处理器在保护模式下的地址翻译(这里主要阐述分段部分的翻译过程,并且主要是为了引入shadow register,为下面的文章作铺垫)(知识准备)
由于80286只是一个过渡产品,生存时间也很短,不具有很强的代表性,不作为重点讲述,我将重点放在386及其以后的32位处理器(统称为 80x86或x86)的寻址上。32位的x86处理机有了实模式,保护模式和V86模式等三种工作模式,由于启动部分不涉及V86模式,所以对该部分也不 作重点讲述。另外在这里我不详细阐述x86的整个存储系统支持,相关的内容可以在手册上和其它的一些书籍上找到。
我们来看32位保护模式的寻址机制。为了以最小的篇幅介绍相关的知识点并尽早切入重点问题,我先给出一幅x86保护模式下的地址翻译图:
图2 x86保护模式下的地址翻译过程
该地址翻译过程分为两部分,即Segmentation部分(分段部分)和Paging部分(分页部分),其中Paging部分是可选的。 Segmentation部分是必须的,该部分的翻译过程如下:首先从指定的段寄存器中取出选择子(selector),然后根据selector中的 TI位确定是要检索GDT(全局描述符表)还是LDT(局部描述符表)中的描述符,不妨假设是要检索GDT中的描述符,然后再根据GDTR寄存器所指示的 GDT的基址以及selector中的index字段所指示的描述符的索引号在GDT中找到相应的描述符,将描述符中的特权级字段DPL与 selector的RPL字段进行比较,看是否有越权访问发生,另外还要检查虚拟地址中的偏移量是否超过了描述符中的limit字段所定义的段长限,这些 检查都通过后,再将描述符中的段基址Base与虚拟地址中的偏移量offset相加形成线性地址。为了便于理解,下面提供了selector和段描述符的 格式:
图3 selector格式
第 0 byte
第 1 byte
第 2 byte
第 3 byte
第 4 byte
第 5 byte
第 6 byte
第 7 byte
Segment Limit
15…0
Segment Base
23…0
Attributes
Base
31..24
Attributes中的数字表示所占bit数
TYPE
3
S
1
DPL
2
P
1
Limit
16…19
A
V
L
0
1
D
1
G
1
图4 段描述符格式
上面只是理论上的情况,在实际的实现中对于每个段寄存器都增加了一个相应的shadow register(称着段描述符高速缓冲寄存器或者影子寄存器),每个shadow register都用来存一个完整的描述符,这个描述符是与之相关的那个段寄存器所装的selector所指示的描述符,shadow register的格式如下:
Base 32 bit
Limit 32 bit
Attr 12 bit
图5 shadow register格式
有了shadow register,就只需要在将选择子载入段寄存器时到描述符表中读一次段描述符,并将其存入shadow register中,以后只要段寄存器中的选择子不改变,就再也不需要到描述符表中去索引描述符了,而直接使用shadow register中的描述符形成线性地址,效率大大提高。但还有一个地方不是很明确,那就是intel似乎从来没有在手册上说过shadow register的limit字段是32bit的。实际上,在描述符中的limit字段只有20bit,而是通过一个粒度字段G来说明每个单位是 1Byte还是4K,从而达到指示1M limit或4G limit的目的。那么这个shadow register中的limit是不是也是20bit,然后由Attribute字段中的粒度位来指示它的单位呢?也就是说shadow register中的字段和描述符中的字段是不是完全一样呢?这还存在一些争议。不过我个人认为,把shadow register的limit字段设计成32bit更合理一点,因为在指令执行时需要用到limit的时候非常多,如果每次都从shadow register中取出20位,然后再取出粒度位计算出一个32bit的limit,这显然在做高频率的重复劳动,还不如直接把shadow register的limit域设计成32bit,这样只需要在将描述符装入时计算一次,得到一个32bit的limit,以后直接使用就行了。不过这可 能会带来一些兼容性问题,例如在64位处理器的情况下,又会怎么样呢?需要进一步的分析和研究。
四、x86 32位处理器对实模式地址形成机制的模拟(关键)
这些shadow register对我们很重要。这种重要性不单单来自效率方面。如果情况真像我下面所说的那样,那么我们不得不改变原来所固化在脑子里的很多对实模式地址 翻译机制的理解。
我们都知道在8086的实模式下是将16位段地址左移4个二进制位再加上16位偏移地址形成20位的物理地址。而80286及其后续产品在保护模式 下是这样形成物理地址的:理论上是通过虚拟地址中的选择子索引到描述符表中的描述符,再将描述符中的基地址加上虚拟地址中的偏移量形成32位的线性地址 (暂不考虑64位的情况),如果不分页,这个线性地址就是物理地址了;实际情况是,直接使用cpu内部的shadow register中的描述符而得到线性地址,而不是每次都去访问内存中的描述符表。那么现在的问题是,这种具有了保护模式的cpu在实模式下是不是也按照 8086的方式来形成物理地址呢?或是自己采用另外一套方案,只是模拟出8086的效果来呢?很多迹象表明,它选择的是后者。
其中一个最有力的证据就是可以在具有保护模式的x86 cpu(286一般先不考虑)的实模式上,不使用任何内存扩展程序的前提下可以访问到4GB的物理内存空间,并且我已经亲手做成了这个实验。这是怎么回事 呢?难免让人糊涂。按理说如果保持向前完全兼容的话实模式下是只能访问1M物理内存空间的,就算打开A20,也就再多加64K-16Byte而已,所以说 x86 cpu对8086在实模式下的行为只是一种模拟,而不是完全相同的。下面我来谈谈这些具有保护模式的x86 cpu是怎样模拟8086的实模式的。
首先从我的实验说起。我事先构建了一个具有三个描述符的描述符表GDT,其中第一个是NULL描述符,这对x86 cpu来说是必须的(至于原因我就不再这里多说了)。第二个是一个数据段描述符,它的Base被设成0,limit被设成4GB。第三个是一个代码段描述 符,Base被设成准备在保护模式下执行的代码的入口点,limit被设置成64K。因为我准备切换到保护模式,所以先打开A20,否则不能访问到所有内 存地址,这在前面有详细的阐述。当CR0中的保护模式允许位被打开后,我用一个长跳转(该长跳转后面带的虚拟地址的选择子指向上面所说的代码段描述符,虚 拟地址中的偏移设置成0,也就是jmp far 16:0这样的形式,由于该描述符的Base被设置成准备在保护模式下执行的代码的入口点,它再加上一个虚拟地址中的偏移0,其线性地址还是准备在保护模 式下执行的代码的入口点)正好跳到准备在保护模式下执行的代码的入口点,最主要的是该长跳转将指定的选择子更新到CS寄存器中,并将选择子指定的描述符更 新到CS寄存器的shadow register中。这个隐藏在背后的更新shadow register的动作我们时刻都不能忘记,后面很多的内容都涉及到这一点。
然后在保护模式的代码中,我将上面的数据段描述符对应的选择子载入了DS,ES,FS,GS等寄存器,同样这个数据段描述符就被载入了上面那些段寄 存器的shadow register中,也就是说这些段寄存器都指向一个4G段了。然后马上切换回实模式。另外,我们再留意一个细节,其实如果在保护模式下只干这么一点点事 的话,我们完全没有必要为CS再准备一个描述符,也没有必要用长跳转去更新CS寄存器以及其相关shadow register的内容,因为即使让CS保持着在实模式下的值去执行保护模式的这一点点无关痛痒的代码也是不会有问题的。因为如果更新了CS的值,那么下 次切换回实模式时还得先找回一个返回到实模式执行代码的返回点,这个返回点显然在切换进保护模式之前就应该保存起来。所以说,其实在保护模式下不改变CS 的值会得到更精炼的代码,只不过别人看起来会有些晕。
好,现在又回到了实模式。这时,让人吃惊的事情发生了,我居然可以使用DS,ES,FS,GS这些段寄存器再加上一个32位的偏移访问到4GB以内 的任何物理内存。怪哉!
怎么来解释这种现象呢?用段地址左移4位加偏移肯定是不能说明问题了,因为理论上在实模式下是根本不能使用32位偏移的,并且如果不是我这一通从实 模式到保护模式,然后又从保护模式回实模式的折腾,在平常一般的实模式的实际环境下也是的确访问不了超过1M的内存的(当然开启A20可以多访问64K- 16Byte,但仅此而已)。
问题还不明朗,等我再把实验往下进行一步。那就是在回实模式后,重新把段寄存器加载一次,如像下面这样更新DS寄存器:
PUSH CS
POP DS
之后再使用DS加32位偏移去访问超过1M的内存就不能获得成功了。究竟是哪个东西有这么大的威力,显然仅仅通过改变DS寄存器的值不至于会造成这 么大的差别,只有一个东西具有这么大的神通,那就是shadow register。每次更新段寄存器都会更新它,并且我们知道,在保护模式下,自从更新完一个段寄存器后,该段寄存器里的选择子除了有一个RPL域用于特 权检查外,其它情况下,该段寄存器都只是一个摆设,因为真正使用的是它的shadow register,因为里面装着宝贵的描述符。难道实模式下也是这样的情况吗?或许真是这样,因为只有这样我们才能够解释很多现象和问题,虽然我们从没有 听说过在实模式下还有什么段描述符。
我用一整套看起来还算合理的理论来解释这一串现象。事实上我们可以认为x86在保护模式和实模式下使用近乎相同的方式来形成地址(当然抛开分页机制 不谈,因为在实模式下是不能分页的)。也就是说,在实模式下也会使用shadow register。只不过它使用的方式与保护模式不同:在保护模式下更新一个段寄存器时,会有相应的段描述符去更新相应的shadow register;而在实模式下更新一个段寄存器时,不会有相应的段描述符去更新相应的shadwo register,而是由cpu按照固定的程序去更新相应的shadow register。这个固定的程序是什么样的呢?至少下面的方式应该是合理的:比如说我们在实模式下给DS赋值F000h,那么cpu会将F000h送入 DS寄存器,并将F000h左移4个二进制位形成一个32位的Base(高12位置0,低20位为F0000h )放入DS的shadow register中的Base域,而limit域直接放入FFFFh,以形成64k的段长限,属性域再填入相关内容(属性域应该怎么填,还有待进一步研 究)。当我们使用如下的方式去访问DS段时:
mov ax, word ptr ds:[si] ;假设si中的值是FFF0h
那么cpu会直接将DS对应的shadow register中的Base域取出来再加上si中指示的偏移FFF0h而形成物理地址FFFF0h,很显然由于在实模式下没有各种特权检查,所以段寄存 器本身更加像一个摆设了。又有一个需要考虑的细节,虽然在实模式下装载段时会自动给register shadow一个64K的limit,但如果人为的使用一个超过64K的offset会有什么后果呢?我的测试结果是直接导致死机。
正是因为x86 cpu在实模式下有这样一种形成地址的机制(这显然不同于8086在实模式下形成地址的机制,而只是一种对它的模拟),所以就可以解释,为什么我们进入保 护模式将段break成4GB后,回到实模式下还会生效,因为那事实上是register shadow的内容还保持着在保护模式下设置的值而在实模式下这些值继续被利用上了而已。至于为什么段寄存器在实模式下被重新加载了以后,又回到了64K 的limit,那是因为register shadow又按照实模式的规则更新了的缘故。另外,利用register shadow的这种地址形成功能,我们还可以解释一个令人困惑的地方。那就是我们切换进保护模式(将CR0的PE位置1)之后,更新保护模式下的CS之 前,可能还会有几条指令,但至少有一条指令(那就是跳转到保护模式的代码段和偏移的那条远跳转指令)按理说这一条指令或几条指令应该按照保护模式的地址形 成机制来运作了,也就是说此时的CS被当作一个选择子来索引描述符了,但是此时的CS还是实模式下的CS,它显然不是一个选择子,假设它的值是 F000h,如果把它的值当作一个选择子去描述符表中找描述符,那结果显然是不堪设想的,而正是由于实模式和保护模式在不更新段寄存器的值时都按 register shadow形成地址这么一个游戏规则,才保证了这时仍按当前register shadow中的内容来寻址并找到适当的指令执行这样一个美好的结果。使实模式和保护模式顺利交接。保护模式切换回实模式时,存在着类似的问题,也存在着 同样美妙的解决方案。
五、X86 CPU在BIOS初始化过程中所处的状态以及BIOS对这种状态的利用(关键)
有了前面这些分析作为基础,我们就可以来看看BIOS了。这里是CPU执行第一条指令的地方。此时的处理器处在一个什么样的状态下呢?BIOS又怎 么样来利用和处理这些状态呢?下面我将一步一步解开这些疑团。以下有一些内容来自我在oldlinux论坛里的发帖,我在那里的用户名是 herochen,不过我在那些帖子中的阐述有一些不正确之处,在这里全都更正过来。
众所周知,机器开始执行的第一条指令不是在内存ram里,而一般是做在固件rom里,因为我们知道ram在断电后是会丢失数据的。关键是这块固件 rom在统一地址空间中如何编址。以前的8086年代,由于使用实模式,地址不能超过1M,所以将这块固件rom编址在1M的最后64k,即它的段地址是 0xF000,这样就不至于把RAM分成两段。而一般有一个习惯,就是将机器执行的第一条指令放在F000h : FFF0h处,一般象下面这样安排BIOS启动代码:
F000 : 0000:
:
F000 : XXXX:
:
F000 : FFF0: jmp F000 : XXXX
这样安排的好处是,让启动固件的地址尽可能靠后,事实上F000 : FFF0离实模式的极限地址F000 : FFFF只有16个字节了,也就安排得开一个跳转指令和其他一些额外信息。而实际究竟需要使用多长的启动代码由jmp F000 : XXXX中的偏移XXXX来把握,如果使用得多,XXXX就小,使用得少,XXXX就大,这样使启动代码尽量靠后,而不浪费多余的地址空间,由于地址空间 安排在最后,也不会把整个地址空间隔离成两段。
但出现80386(更准确的说应该是80286)后,麻烦就来了,由于80386的保护模式可以使用超过1M的地址空间,如果把冷启动固件编址在 F000h段内,就会把整个地址空间隔离成不连续的两段,一段是F000h以前的地址,一段是1M以后的地址,这很不方便。intel采用的办法是,还是 默认将执行启动代码的BIOS ROM编址在系统可寻址空间的最后(如32位x86机的话,这段地址就位于4GB的最后一个64K内),在系统复位时,CPU进入实模式,并将CS寄存器 设置成F000h,而将它的shadow register的Base设置成FFFF0000h(理论上正常情况下CS为F000h的话,其shadow register的Base应该设置成000F0000h,但intel有意识的将高12位触发成1了,除了这样他也没有什么好办法让机器一启动就跑道 4GB那么高的地址上去执行),而偏移量EIP置成0000FFF0h,所以机器执行的第一条指令的固件安排的物理地址显然就变成了FFFFFFF0h。 BIOS代码和以前还是要兼容的,也就是说此时从FFFFFFF0h处取出的还是一条远跳转指令jmp F000 : XXXX(我跟踪调试过好几款BIOS,这里的XXXX似乎都是E05B),问题随之而来。这个远跳转指令是要更新CS寄存器和它的shadow register的,也就是说执行这条jmp F000 : E05B之后(也就是CPU执行第一条指令之后),CS将被更新成F000,其实CS原来就是这个值,这里说不上是更新,但CS的shadow register就不一样了,它被真正的更新了,它的Base域被更新成000F0000h了(高12不再具有触发成1的功能,那个功能只在机器启动到第 一次更新CS的内容期间有效)。这个Base再加上虚拟地址中的偏移量E05B,得到物理地址000FE05Bh,这就是CPU执行的第二条指令的地址, 但是这条指令的地址已经是1M以内了。但我们不要忘记,这时的F000h段内可不再是BIOS ROM了,这一段此时安排的事实上是我们的RAM空间,这一段RAM需不需要初始化才能使用那还另说,关键是此时此刻这个地方不应该有可以执行的代码才对 啊?CPU第二条指令就跳到这里不是自寻死路吗?
似乎走进了死胡同,但我翻阅了很多资料,找到了一点线索。在很久以前出现过一个叫着Chips & Technoloqies的公司,他设计出一组被称着neat的芯片组,可以将内存高端的BIOS ROM映射到1M以内的RAM空间里,并且可以使这一段被映射的RAM空间具有与ROM类似的只读属性。这个公司后来被intel收购。但后来这种映射似 乎就成为了一种标准。由于这种映射关系我们有理由相信,机器启动的时候,4G的最后一个64K里与1M的最后一个64K里应该具有相同的东西,所以即使从 FFFFFFF0h用一条jmp跳到000FE05Bh,也仍然能够找到正确的代码去执行。
那么BIOS接下来要干一些什么事呢?它有很多事情要做,我只举几件有代表性的,其中有两件事是DRAM的初始化和memory sizing。按理说这个时候CPU还处在实模式下,BIOS还没有办法去确定超过1M的内存量。另外还有一件事就是代码和数据拷贝,因为映射到1M以内 来的BIOS ROM容量有限,事实上还有很大一部分没有映射过来,以压缩的形式存放在高端的ROM中了,BIOS在1M以内执行初始化时难免需要将高端的那些内容拷过 来使用,这也是不容易做到的。但不要忘了,我们可以使用前面说的将段break成4G的方法来做成这几件事。当然,似乎还存在着这样一种可能性,那就是切 换到保护模式,这些事情就都可以做了,并且好像没有必要再切换回实模式。情况没有想象中那么简单,从我前面的那个实验看,我切换到保护模式之后只执行了几 行非常必要的将段break成4G的代码,其他的事情一律不做,因为保护模式下有非常严格的特权检查,并且需要设置GDT,IDT,LDT等一系列的表 格,一般的代码是不容易在保护模式下跑起来的,所以想在保护模式下完成整个BIOS的初始化,工程过于浩大,几乎等于写一套小型的保护模式操作系统了 (FreeBios可能就是这么干的)。
当然我也有足够多的证据证明我们常用的BIOS都使用了这种break limit的技术,并且它们完成break后都是迅速切换回实模式。以下是几款BIOS的一些代码资料,我们可以窥见一斑。
先来看AMI BIOS。网上可以找到关于AMI BIOS的一部分源代码(但似乎不是很完整):
jmp_si go_to_flat_mode ; DS=ES=0 (4GB limit),
;GateA20 enabled
check_point_ini 0d3h ; ======== D3
jmp u_cp_init ; init all chipset register
u_cp_init_end:
check_point_ini 0d4h ; ======== D4
;---------------------------------------;
; 512 KB MEMORY TEST ;
;---------------------------------------;
xor ax,ax
mov es,ax
mov bx,es:word ptr [472h] ; save 40:72
这段代码很简单,其中jmp_si是一个宏,是将jmp的下一条指令的地址放入si,以备返回用(因为这时内存还没有初始化,还不能使用堆栈,所以 不便于用call指令),再看看该语句后面的注释,基本上可以认为go_to_flat_mode这段代码就是作break limit用的(遗憾的是该段代码没有提供,所以只能猜测,不过从上下文看,也是八九不离十)。随后的check_point_ini是输出阶段码供调试 用,而jmp u_cp_init是去做内存初始化和memory sizing。好,关键是看看最下面三行,我们假设此时还是在保护模式下没有回到实模式,那么最后的三条指令就是将一个NULL选择子载入ES寄存器,并 且最后一条语句是使用ES去访问内存单元,这毫无疑问是会出异常的,因为NULL selector是作占位用的,不能真正去使用它。所以我们据此一点就可以断定,此时已经回到了实模式。
再看AWORD BIOS。我没有得到AWORD BIOS的源代码,我是用工具将其反汇编出来的。AWORD BIOS里切换进保护模式的次数很多,这正好从一个侧面反映了它每次切换进保护模式后又回到了实模式,我只举一处。代码中有这样一个子过程 sub_178:
sub_178 proc near
mov ax,cs
mov ds,ax
mov si,40B8h
lgdt fword ptr [si] ; Load global des tbl
mov eax,cr0 ; Mov reg-control reg
or al,1
mov cr0,eax ; Mov reg-control reg
jmp short loc_458
db 8Bh,0C0h
loc_458::
mov ax,10h
mov ds,ax
retn
sub_178 endp
sub_178过程显然执行了从实模式切换进保护模式的动作,但似乎并没有从保护模式返回实模式,千万不要以为我们已经找到了疑点。让我们看看程序 对sub_178的调用:
sub_180 proc near
:
:
call sub_178
:
:
call sub_179
:
:
retn
sub_180 endp
有call sub_178出现的地方,后面总会跟一个call sub_179,sub_179又是何物呢?
sub_179 proc near
mov eax, cr0 ; Mov reg-control reg
and al, 0FEh
mov cr0, eax ; Mov reg-control reg
jmp far ptr loc_ret_459
loc_ret_459::
retn
sub_179 endp
很显然sub_179不干别的,就是切换回实模式。代码胜于雄辩。J
再来看BOCHS的虚拟BIOS。BOCHS的BIOS的所有反汇编代码中,用到CR0的地方仅仅4处,正好是前面两处用于切换到保护模式,而后面 两处用于切换回实模式:
lgdt fword ptr es:[si+8] ; Load global des tbl
lidt fword ptr cs:data_316 ; Load int des table
mov eax,cr0 ; Mov reg-control reg
or al,1
mov cr0,eax ; Mov reg-control reg
;* This jump is a protected mode jump to flush the pre-fetch queue -
;* - the segment selector was ignored.
jmp far ptr loc_374
loc_374::
mov ax,28h
mov ss,ax
mov ax,10h
mov ds,ax
mov ax,18h
mov es,ax
;* xor si,si ; Zero register
db 31h,0F6h ; Fixup - byte match
;* xor di,di ; Zero register
db 31h,0FFh ; Fixup - byte match
cld ; Clear direction
rep movsw ; Rep when cx >0 Mov [si] to es:[di]
mov ax,28h
mov ds,ax
mov es,ax
mov eax,cr0 ; Mov reg-control reg
and al,0FEh
mov cr0,eax ; Mov reg-control reg
jmp far ptr loc_375
loc_375::
lidt fword ptr cs:data_317 ; Load int des table
;* xor ax,ax ; Zero register
db 31h,0C0h ; Fixup - byte match
mov ds,ax
非常的明了!
还有Vmware的虚拟BIOS,情况也都类似,我就不再重复举例了。
到这里,我希望已经将一些基本的问题都讲清楚了,当然只是希望而已。J 文中可能还有很多的纰漏,请大家多多批评指正!