本文侧重点在于BIOS查找“启动顺序”(Boot Sequence)之前,也就是从按下电源到BIOS移交权限之间的这一段
我们从按下电源开始。
首先,是CPU Reset。主板加电之后在电压尚未稳定之前,其北桥控制芯片会向CPU发出重置信号(Reset),此时CPU进行初始化。当电压稳定后,控制芯片会撤销Reset信号,CPU开始工作。我们要探讨的第一个问题就是CPU执行的第一条指令的位置
。
现在网上流传的资料基本上是8086 CPU的资料,给出的说法一般是这样:CS寄存器初始化为0xF000,IP寄存器初始化为0xFFF0,所以按照CPU实模式地址计算法则,CPU执行的第一条指令地址是CS*10h+IP,即0xFFFF0处。8086CPU确实如此,但是80386及其以上的CPU
其计算地址法则却不是这样。
第一点,80386及其以上的现代CPU(以下简称CPU)加电Reset之后并不是直接进入实模式;
第二点,CPU在合成地址的时候不区分实模式和保护模式。
我们知道,CPU进入保护模式的方法是CR0寄存器的PE位置为1。而在CPU刚加电的时候,CR0寄存器的PE位确实没有置1,那么,此时是实模式吗?暂时还不是,Intel并没有给给出表示此时CPU状态的术语名词,我们姑且称之为混沌模式吧。自从80386以来,因为增加了保护模式的缘故,CS等段寄存器不再是简简单单的段寄存器了,而是一个包含了段选择器(segment selector)、段基址(segment base),以及段限制(segment limit)的一组复杂寄存器。显然段基址决定着内存段的基地址。不过需要说明的是作为程序员只能操作CS寄存器中的“段选择器”这16位的大小,其它的区域作为隐藏区域对程序员不可见,我们无法访问。
当CPU处于段寻址模式的时候,假设段选择器(我们能访问的那16位)装入了0xF000,那么CPU会先将F000 * 10h也就是F0000h装入段基址里。之后需要合成地址的时候不考虑别的,而是直接从之前合成好的段基址里读出基地址F0000h加上IP寄存器里的偏移生成地址。如果CS寄存器的值不发生改变,段基址部分就不会发生改变。所以我们说,CPU在合成地址的时候不区分实模式和保护模式,CPU只是机械的从隐藏区域读出来段基址和IP寄存器的数值相加。
Intel这样做的目的何在?当然是为了效率,也许实模式的地址计算很快,但是保护模式计算一个地址还要有去内存中寻找段描述符等工作,这会大大影响CPU的效率,而我们知道,程序具有访问局部区域里的数据和代码的趋势(局部性原理)。所以在CS寄存器没有发生变化的时候,直接从之前隐藏区域获取段基址岂不是更快?当CS寄存器被修改呢?那CPU就再进行一次查找段描述符的操作,然后更新隐藏区域。
顺便说一句,利用CPU的这个特性,我们可以先进入CPU的保护模式配置好某个起始地址为0,段限长为4G的段描述符并加载到除过CS的其他任意寄存器(CS寄存器改变太过频繁),然后退出保护模式进入实模式去执行指令,利用80386之后长达32位的ESI或EDI寄存器和那个段寄存器配合寻址就可以在实模式下访问全部地址空间了。(中途不可以修改那个载入过段描述符的寄存器,否则其隐藏区域会被更新)。这种方式是一种称之为Big Real Mode的模式,其区别于实模式和保护模式。
说了这么多,我们找张图片大概说明一下CS寄存器。下图来自Intel的“CPU使用说明书”,著名的那三卷开发文档的第三卷《Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3A: System Programming Guide》P91中的插图,算是对上文的证明,至于隐藏区域有多大,Intel并没有说,不过不会短于一个段描述符的大小,也就是至少有8字节(64位)。(纯属猜测)
CPU在Reset之后,IP寄存器被置为0x0000FFFF,CS寄存器的段选择器默认值是0xF000,而隐藏区域中的段基址却没有按照实模式的标准去装入,而是被置为0xFFFF0000。那么CPU生成的第一条访问地址是什么呢?显而易见,80383之后的CPU合成的是0xFFFFFFF0这个地址,这也符合Intel文档的说法。也有资料显示从80286之后的CPU就是这个地址了,暂时没有考证。即便是有其它文档有不同见解,还是觉得Intel的说法更靠谱,毕竟还是人家造的处理器不是。
再来一张图,说明下80386以上CPU在保护模式下的寻址图,同样来自那个PDF。
所以我们说CPU执行第一条指令的时候不是实模式也不是保护模式,而是一种怪异的中间模式。
问题又来了,这一条指令在哪?我们知道计算机开机后首先读取哪里呢?BIOS!对就是它,这一条指令会被指向BIOS。
我们虽然已经假定过读者知道线性地址空间的大致意思,但我觉得还是有必要简单说一下,IBM PC一部分的端口采用独立编址,而另一部分采用端口统一编址,传统PC机使用0x000~0x3FF共1024个端口地址。现代PC则有多达64KB的 I/O 端口提供编址。不过显存等一些硬件的地址还有所有BIOS的编址却在线性地址空间里。这也是我们所谓的32位操作系统没办法完全利用4G内存的原因,尽管寻址能力有4G(共2的32次方个地址),但是并不是所有的地址都能分配给内存使用。
对于传统的CPU+北桥+南桥类型的主板来说,CPU的地址请求通过FSB(Front Side BUS前端总线)到达北桥,北桥将这个请求送到南桥。而对于最新的主板芯片组来说,北桥和CPU封装在一颗芯片里面,所以会看到这个请求通过DMI/QPI(Quick Path Interconnect,即快速通道互联,是Intel用来取代FSB的新一代高速总线,CPU与CPU之间或者CPU与北桥芯片之间都可以使用QPI相连。在民用级的i7+X58平台,i7处理器与X58北桥芯片之间就通过QPI总线相连)被送到南桥。请求到达南桥后,南桥根据目前的地址映射表的设置决定是否将请求转发到SPI(Serial Peripheral Interface)或者LPC(Low Pin Count)。
这里貌似说的过于底层了,简单说就是南桥芯片拥有一张地址映射表,当有地址解析的请求到来时,南桥查看这张表决定将地址解析到何处去。这张表里有两个特殊区域,一个是从地址空间4G向下,大小从4MB到16MB不等的一个区域,我们以4MB为例,地址空间从FFFC00000h~FFFFFFFFh。称之为Range 4G。第二个区域一般是是从1MB向下128KB的范围,即E Segment和F Segment,从E0000~FFFFF,称之为Legacy Range,也就是说,FFFC00000hFFFFFFFFh之间和E0000FFFFF之间的寻址请求都会被导向到SPI/LPC,最终指向了BIOS。
呼~说了这么多,这个地址总算是指向了BIOS了。解决了第一条指令,接下呢?厂商们有分歧了,Intel设计的EFI(Extensible Firmware Interface)的做法和传统的Legacy BIOS就不一样了。
就Legacy BIOS来说,放在0xFFFFFFF0的第一条指令一般是一个远跳转指令(far jump),也就是说CPU在执行Legacy BIOS时,会直接从0xFFFFFFF0跳回F Segment,回到1MB以下这个Legacy BIOS的老巢里去。而EFI BIOS的第一条指令是wbinvd(清洗CPU高速缓存),之后做一些设定之后,会直接进入保护模式。所以EFI BIOS是从南桥Region 4G通过,并不需要Legacy Region。
必须说明,这里提到的一些说法参考自一些国外论文及其译文,我只能考证其说法而没有办法考证原作者。虽然在博文内容上我要求自己按照论文来写,但是引文上没有办法考证的我就只考证说法的正确性而不注明出处了,大家见谅