CPU执行的第一条指令
计算机上刚刚电后的CPU
当将主板接入电源后,电源就开始向主板上的各个设备供电,此时主板上的CPU并不会马上开始执行命令。以笔记本为例,主板通电之后,EC芯片首先开始工作,EC会向CPU发出一个reset信号并且一直保持这个信号,发出这个reset信号的主要目的是为了让CPU恢复到一个默认的初始状态,而一直维持这个信号是为了等待电源供电稳定,只有EC检测到供电稳定,才会撤掉维持的reset信号。
如果我们通过手工按power button来开机,那么当松开按钮的时候,reset信号就会消失。当reset信号消失,CPU就开始执行指令。由于此过程并非由UEFI来做主要的控制,故不详细说明,详细的过程可以参考CPU上电时序详细分析 这篇文章,个人觉得写的很详细很好
常识告诉我们CPU是执行内存地址上的指令,那刚刚上电的机器是从0 地址开始执行内存上的指令吗?假设是这样的,那矛盾就出现了,那就是在计算机刚刚启动的时候,内存条DIMM 中是没有任何的内容的,因为DIMM是一种下电就会丢失内容的设备,刚上电的时候DIMM中不会又任何的存储信息,所以刚刚上电的时候CPU执行的是什么位置的内容呢?
我们常常说UEFI 是计算机在早期进行自我检测、初始化设备、为OS创建准备环境所执行的一段代码,那么在CPU上电之后是不是立刻执行的就是这一段代码呢?如果是CPU又是如何准确的找到UEFI代码的呢?
以上的疑问可以归为以下三个问题
- 刚刚上电的时候CPU执行的是内存中的内容吗
- 如果不是执行的是什么位置的内容?
- CPU刚刚上电执行的内容和UEFI 代码的关系是?
CPU 执行的第一条指令
首先,第二个问题我们可以从Intel 手册中找到确定答案
The first instruction that is fetched and executed following a hardware reset is located at physical address FFFFFFF0H.
硬件重启后读取并且执行的是位于physical address FFFFFFF0H的指令
现在网上很多的资料、文章还在说第一条指令的位置是FFFF0H,其实这种说法也不能说不对,只能说有局限,应该说8086 是从这个位置读取第一条指令的。现在我想要彻底的说清楚这个问题,理清来龙去脉。
前面介绍MMIO和PMIO的时候,我曾经介绍过在8086的时代,那个时候CPU寄存器是16位的,地址线20根。
在当时这种CPU寄存器的位数小于 地址线位数的情况下,想要CPU控制全部的地址空间就成了一个问题,为了解决这个问题,在8086中物理地址的表示方式如下:
物理地址 = 段基址 +偏移地址
其中 段基址 = 段寄存器的内容×16 (即段寄存器内容左移四位)
这样形成的20位地址数据就能够表示全部物理地址空间了。
而当时CPU只有一种工作模式:实模式(程序中用到的地址都是真实的物理地址,段基址+段内偏移地址产生的地址 就是真实的物理地址 程序员能够直接操作的也是这个实际的地址)
此时最大寻址空间(也就是之前说的 physical address space )为2^20 = 1M
这1M内存范围就是当时8086能够使用的全部了,这一段地址范围不是仅仅给主板上的DIMM使用的,主板上所有的物理的存储都要共享这个空间的,其中就包含了平时常见的DIMM RAM 和BIOS所在的flash。
当时还是BIOS的年代,不同机器的外设不同就需要对应不同的BIOS,哪怕是一个小小的更改都需要准备新的BIOS(具体也不清楚 没接触过leagcy 模式的机器),这就导致BIOS的大小千变万化,但是如果将BIOS全部放在0起始的位置,用户程序能使用的地址空间可能的起始点就非常混乱了。
为了统一方便使用者和开发者,当时就统一将BIOS放在了1M地址空间的最顶部,这样用户使用内存就可以统一从0开始使用。传统BIOS 1M空间的分配如下
但是这样仍旧有一个衍生的问题,用户的使用是方便了,但是CPU想要读BIOS应该从什么位置开始呢?BIOS代码的大小又是各式各样的,其起始地址也不尽相同,为了fix这个问题,统一规定CPU上电之后,统一执行FFFF0位置的指令,在这个位置,放一条jmp命令 ,各家BIOS厂商可以自己设置要跳转地址,让CPU跳转到相应的位置找到你的BIOS代码的起始位置开始执行。
随着计算机以及CPU不断发展,很快就出现了32位CPU ,此时的寻址空间能够达到2^32 = 4G,但是为了兼容之前的许多设计,不得不设计CPU在刚刚启动的时候仍然处于实模式(1M寻址的状态)。随着CPU的发展,64位CPU开始出现,但是为了兼容之前的设计,CPU在刚刚上电的时候仍然坚持实模式启动(详细可以看UEFI SEC阶段启动的过程,也是从实模式开始的)。既然如此按照之前的说法,此时处于实模式下,那能够找到的地址范围应该最大还是FFFF0,为什么说现在机器第一条指令的位置是FFFF FFF0呢?下面详细看Intel手册的说明
翻译如下(个人翻译,如有错误见谅):
硬件重置之后获取并执行的第一条指令位于physical address 0xFFFFFFF0H,这个地址比最高物理地址低16 byte,包含软件初始化代码的EPROM必须位于该地址。
physical address 0xFFFFFFF0H 超过了当CPU处于实模式时候能够支持的最大地址范围 1MB。处理器按照下述步骤将寄存器初始化成起始地址0xFFFFFFF0H。CS寄存器有两个部分 可见段选择部分(the visible segment selector part)和隐藏基地址部分(the hidden base address part.) 在实地址模式下,基地址通常是通过将16位的 segment selector 的值左移四位而生成的。然而在硬件重启期间,CS寄存器中的segment selector 被设置成F000H ,base address 被设置成FFFF 0000H,起始地址因此通过将base address 和 EIP register的值相加得到 (即 FFFF0000 +FFF0H = FFFF FFF0H)
硬件重启完成之后CS register 第一次填充为另外的新的值时,处理器会遵循实地址模式下地址处理的规则(CS base address = CS segment selectore ×16). 为了确保CS register 中的 base register 在 EPROM 中的软件初始化代码完成之前保持不变,初始化代码中禁止包含例如 far jump 或者far call,或者中断操作等这些会导致CS寄存器发生变化的操作。
以上一段话看着比较复杂,其实很好理解,CS寄存器可以分成两个部分:the visible segment selector part 和 the hidden base address part ,这两个部分在CPU reset后分别初始化为 F000H 和 FFFF 0000H 。此时EIP 寄存器仍然初始化为 FFF0H。
CPU上电reset 之后,起始地址的计算方式为:
the hidden base address part + EIP register = FFFF 0000H + FFF0H = FFFF FFF0H
查找完待执行的第一条指令后,CS寄存器就继续填写其他相应的值,此时地址的计算方式就变成:
the visible segment selector part ×16 + EIP register (实模式下的计算方式)
这样在兼容16位实模式的基础上又完成了从FFFF FFF0H读取第一条命令
Intel手册 Vol3A 10-2 P3340 说明了CPU初始化之后部分寄存器的状态
Reset Vector
CPU执行的第一条命令所在的物理地址位置有一个专门的名称 – Reset Vector . 我们可以通过RW看以下这个位置的代码是什么样子,以我的笔记本的读取结果为例
在 FFFF FFF0H位置的代码为 90 90 E9 BB B7
我们根据汇编代码对应来看,汇编语言中90对应的是 No Option,此处9090对应的就是两个nop
E9对应的汇编代码为 jmp,表示无条件跳转指令,后面4 byte表示的是跳转的偏移量
此时我们打开UEFI 代码中 ResetVectorVtf0.asm 文件查看
可以看到当前代码中正好对应我们刚才解析出来的汇编结果,我们现在可以回复前述的第三个问题:CPU刚刚上电执行的内容就是UEFI 的代码,这段代码是UEFI代码的开端,也就是说,CPU上电稳定之后开始执行的第一条指令就是UEFI代码。
执行jmp跳转命令后面就是正式的UEFI代码,开始按照SEC -> PEI ->DXE ->BDS等阶段开始执行相应流程
UEFI前期代码执行
现在就剩下最后一个问题,刚上电的时候CPU执行的是内存中的内容吗?很明显我们现在可以很确定的说,并不是。前面我们已经通过代码与CPU第一条指令位置内容比较知道了: CPU上电之后执行的第一条命令就是UEFI代码中的的第一条,而内存的初始化是在UEFI的PEI阶段中完成的,所以很明显此时并不是从内存中读取代码执行的。那么此时代码是如何执行的呢?答案:片上执行(eXecute in place,XIP).
XIP,通俗的说就是不需要将代码中加载到内存中,直接在flash原地执行相应的代码。有了这种能够原地执行的方式,就能够在内存没有初始化的情况下执行UEFI中的代码了,但是这种执行方式与内存执行相比速度很慢,所以当有了可用内存之后就需要马上开始切换了。