UEFI启动流程:从按下电源到操作系统接管
大家好,我是你们的UEFI讲师。今天我们来聊聊UEFI启动流程中最核心的四个阶段:PEI、DXE、BDS和RT。说白了,这就是从你按下电源键到操作系统完全接管硬件之间,固件到底干了些什么。
我刚开始接触UEFI时,总觉得这些阶段划分得很抽象。后来在项目中调试一次启动失败的问题,才真正理解了每个阶段存在的意义。嗯,咱们一个一个来看。
核心要点:UEFI启动流程分为四个阶段——PEI(平台初始化)、DXE(驱动执行环境)、BDS(启动设备选择)、RT(运行时服务)。每个阶段都有明确的分工和边界。
2.1 PEI阶段:从零开始的硬件发现
PEI(Pre-EFI Initialization)是整个启动流程的第一步。这时候内存还没初始化,CPU可能还处于复位状态。你想想看,固件代码本身都还在Flash里跑,连个像样的栈都没有。
PEI阶段的核心任务就两件:发现内存和初始化CPU。为什么内存这么重要?因为后续所有阶段都需要内存来运行代码。没有内存,DXE阶段的驱动根本加载不了。
个人经验:我在调试一块国产主板时,遇到过PEI阶段卡死的问题。最后发现是SPD(Serial Presence Detect)数据读取超时,导致内存参数获取失败。PEI阶段的容错处理其实很有限,一旦内存初始化失败,整个机器就变砖了。
PEI阶段的工作模式是这样的:
- SEC(Security):安全验证,检查固件完整性。这部分其实在PEI之前,但通常和PEI一起讲。
- PEI Core:加载PEIM(PEI Module),调度执行。
- PEIM:具体的初始化模块,比如内存控制器初始化、CPU微码加载。
- PEI Services:提供基础服务接口,比如HOB(Hand-Off Block)的创建。
PEI阶段结束后,会生成一个关键数据结构——HOB。HOB记录了所有已发现的硬件信息,传递给DXE阶段。我习惯把HOB比作「接力棒」,PEI跑完第一棒,把硬件信息写在HOB里,DXE接过来继续跑。
注意:PEI阶段的内存非常有限,通常只有几十KB的临时内存(Cache as RAM,简称CAR)。不要在PEI阶段做大量数据操作,否则很容易栈溢出。我曾经见过一个同事在PEIM里申请了8KB的缓冲区,直接把CAR撑爆了,机器反复重启。
2.2 DXE阶段:驱动的大舞台
DXE(Driver Execution Environment)阶段,说白了就是UEFI驱动的「主战场」。这时候内存已经可用,固件可以加载各种驱动了。
DXE阶段的核心是DXE Dispatcher,它负责扫描所有固件卷(FV,Firmware Volume)中的驱动,按照依赖关系依次加载执行。每个驱动注册自己的Protocol,供其他驱动调用。
我举个例子:
// 一个典型的DXE驱动入口
EFI_STATUS
EFIAPI
MyDriverEntryPoint (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
// 注册Protocol
gBS->InstallProtocolInterface(
&ImageHandle,
&gMyProtocolGuid,
EFI_NATIVE_INTERFACE,
&mMyProtocolInstance
);
return EFI_SUCCESS;
}
这段代码看起来简单,但背后涉及很多细节。比如gBS是Boot Services表,它提供了内存分配、事件管理、Protocol注册等核心服务。DXE阶段的所有驱动都依赖这些服务。
DXE阶段结束时,系统应该已经具备了完整的硬件抽象层。所有关键硬件(磁盘、USB、PCIe、网络等)的驱动都已加载,Protocol都已注册。这时候,BDS阶段就可以上场了。
| DXE核心组件 | 功能描述 | 我踩过的坑 |
|---|---|---|
| DXE Dispatcher | 按依赖顺序加载驱动 | 依赖循环导致死锁,需要手动调整DEPEX |
| Boot Services | 内存、事件、Protocol管理 | 忘记释放内存导致SMM泄露 |
| Runtime Services | 操作系统可调用的固件接口 | SetVariable在RT阶段调用失败 |
| Protocol | 驱动间通信的接口标准 | Protocol GUID写错,驱动找不到对方 |
2.3 BDS阶段:选谁启动?
BDS(Boot Device Selection)阶段,名字已经说得很清楚了——选择启动设备。DXE阶段把所有驱动都准备好了,BDS阶段要做的就是枚举所有可启动设备,然后按照用户配置的启动顺序去尝试启动。
BDS阶段的核心流程:
- 枚举启动设备:扫描所有磁盘、USB、网络等设备,检查是否有可启动的EFI分区。
- 读取BootOrder:从NVRAM中读取用户配置的启动顺序。
- 尝试启动:按顺序尝试每个启动项,直到成功或全部失败。
- 加载Boot Manager:如果找到可启动设备,加载其EFI应用程序(比如bootmgfw.efi)。
你可能会问:如果所有启动项都失败了呢?嗯,这时候BDS会进入Boot Manager Menu,也就是我们常说的BIOS设置界面。用户可以在里面调整启动顺序、修改硬件配置。
关键概念:BDS阶段结束后,控制权会交给操作系统的Boot Loader。此时UEFI固件的大部分功能会退出,只保留Runtime Services供操作系统使用。
2.4 RT阶段:固件的「售后服务」
RT(Runtime Services)阶段,是UEFI固件在操作系统运行期间提供的「售后服务」。操作系统可以通过UEFI Runtime Services接口,调用固件的某些功能。
常见的Runtime Services包括:
- GetVariable / SetVariable:读写NVRAM中的UEFI变量。
- GetTime / SetTime:读取/设置系统时间。
- ResetSystem:重启或关机。
- UpdateCapsule:固件更新。
我个人觉得,RT阶段最容易出问题的地方是内存映射。操作系统启动后,固件代码和数据必须保留在物理内存中,但操作系统可能会重新映射内存。如果固件代码访问了错误的地址,系统就会崩溃。
避坑指南:我曾经在开发一个UEFI驱动时,在RT阶段调用了Boot Services的AllocatePool函数。结果操作系统直接蓝屏了。原因很简单——RT阶段只能调用Runtime Services,Boot Services已经不可用了。这个错误在调试时很难发现,因为模拟器里可能不会触发。
RT阶段的另一个特点是虚拟地址转换。操作系统启动后,会把物理地址映射到虚拟地址空间。UEFI固件需要支持这种转换,否则无法在操作系统环境下正常工作。这部分涉及SetVirtualAddressMap()函数,我们后面会专门讲。
2.5 四个阶段的衔接与数据传递
四个阶段不是孤立的,它们通过HOB和System Table传递数据。PEI阶段生成HOB,DXE阶段读取HOB并构建System Table,BDS阶段使用System Table中的Protocol,RT阶段保留System Table中的Runtime Services。
我画了一张图来展示这个数据流:
理解这个数据流很重要。我在调试一个启动失败的问题时,发现PEI阶段生成的HOB中,内存描述符的地址范围写错了。DXE阶段读取后,分配内存时直接覆盖了固件代码区,导致系统崩溃。这种问题很难定位,因为错误发生在PEI阶段,但症状在DXE阶段才显现。
2.6 小结
UEFI启动流程的四个阶段,每个阶段都有明确的职责:
- PEI:从零开始,初始化内存和CPU,生成HOB。
- DXE:加载所有驱动,构建完整的系统服务。
- BDS:选择启动设备,加载操作系统Boot Loader。
- RT:操作系统运行期间,提供固件服务接口。
我个人觉得,初学者最容易犯的错误是把四个阶段混为一谈,在PEI阶段调用DXE阶段的API,或者在RT阶段使用Boot Services。记住一句话:每个阶段只能使用本阶段及之前阶段提供的服务。这个原则贯穿整个UEFI开发,务必牢记。