UEFI固件架构:从SEC到RT的完整旅程
UEFI固件的启动过程,说白了就是一台电脑从按下电源键到操作系统接管前的全部秘密。我做了这么多年固件,见过太多工程师只盯着自己那一亩三分地——写PEI的不知道DXE在干嘛,做BDS的看不懂SEC的代码。这样不行。
今天我把UEFI的六个阶段掰开揉碎了讲给你听。每个阶段做什么、怎么衔接、有哪些坑,咱们一次说清楚。
核心要点:UEFI启动分为SEC→PEI→DXE→BDS→TSL→RT六个阶段,每个阶段都有明确的职责边界和交接协议。理解这个架构,是做好固件开发和安全启动的基础。
SEC阶段:一切从这里开始
SEC(Security Phase)是CPU复位后执行的第一个代码块。这时候内存还没初始化,连C语言都跑不了,只能用汇编。
SEC阶段干三件事:
- 建立安全信任根——验证固件本身的完整性。我参与过一个项目,客户要求从SEC就开始做签名校验,结果发现CPU的缓存太小,验签代码放不下。最后只能把验签拆成两段,先验头部再验主体。
- 初始化临时内存(Cache-as-RAM)——把CPU缓存当内存用。嗯,这里有个坑:不同CPU的Cache-as-RAM实现方式不一样,Intel和AMD的寄存器配置完全不同。
- 传递启动参数给PEI——通过寄存器或固定内存地址,把CPU状态、复位原因等信息传给下一阶段。
个人经验:SEC阶段的代码量通常不超过4KB。我建议你写SEC时尽量精简,能用汇编就别用C。有一次我看到有人把printf库链接进了SEC,结果固件体积直接爆了——SEC里根本没有控制台,printf打给谁看?
PEI阶段:内存来了,C语言来了
PEI(Pre-EFI Initialization)阶段的任务很明确:把内存初始化好,为DXE铺路。
PEI的核心组件是PEIM(PEI Module)。每个PEIM负责一件事,比如:
| PEIM名称 | 职责 | 常见问题 |
|---|---|---|
| CPU PEIM | 初始化CPU微码、缓存、频率 | 微码版本不对会导致系统不稳定 |
| Memory PEIM | 检测并初始化DRAM | 内存时序参数写错直接不开机 |
| Platform PEIM | 配置芯片组、GPIO、时钟 | 不同板卡配置差异大,容易漏配 |
PEI阶段通过PPI(PEIM-to-PEIM Interface)通信。说白了就是一个函数指针表,一个PEIM注册服务,另一个PEIM调用它。
我曾经遇到过一个诡异的bug:某款主板在低温环境下内存初始化失败。查了两周才发现是Memory PEIM里一个时序参数写死了,没考虑温度补偿。从那以后,我所有涉及硬件的代码都要求加上参数校验。
警告:PEI阶段的内存是临时性的。一旦DXE接管,PEI分配的内存就会被回收。如果你有数据要传给DXE,必须通过HOB(Hand-Off Block)机制。我见过有人用全局变量传数据,结果DXE一启动数据全丢了——全局变量在PEI结束后就失效了。
DXE阶段:驱动大本营
DXE(Driver Execution Environment)是UEFI最庞大的阶段。所有设备驱动、文件系统、网络协议都在这里加载。
DXE的核心是DXE Dispatcher,它负责:
- 扫描所有固件卷(FV),找到DXE驱动
- 检查驱动的依赖关系(DEPEX)
- 按依赖顺序加载并执行驱动
- 驱动注册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;
}
DXE阶段还有一个重要角色——安全启动(Secure Boot)的签名验证就在这里执行。每次加载驱动或镜像时,DXE Dispatcher都会调用安全启动策略,检查签名是否有效。
我建议你在写DXE驱动时注意两点:
- DEPEX依赖别写太复杂,否则Dispatcher会花大量时间解析依赖图
- Protocol注册后记得检查返回值,我见过有人忽略返回值导致后续调用崩溃
BDS阶段:用户看到的第一眼
BDS(Boot Device Selection)阶段负责展示启动菜单、选择启动设备、加载操作系统加载器。
BDS做的事情:
- 枚举所有可启动设备(硬盘、U盘、网络、光驱)
- 按启动顺序尝试加载
- 如果安全启动开启,验证启动加载器的签名
- 如果所有设备都失败,进入错误处理
这里有个常见的误解:很多人以为BDS就是显示Logo和菜单。其实BDS的核心工作是启动策略管理。Logo显示只是最表层的功能。
避坑指南:我曾经遇到一个项目,BDS阶段启动U盘总是失败。查了半天发现是BDS在枚举USB设备时超时时间设得太短——USB设备枚举需要时间,尤其是老旧的U盘。解决方案是把超时从100ms改成500ms,问题解决。
TSL与RT阶段:交接给操作系统
TSL(Transient System Load)阶段是UEFI和操作系统之间的桥梁。操作系统加载器(如Windows的bootmgfw.efi或Linux的shim.efi)在这个阶段运行。
TSL的关键动作:
- 加载操作系统内核到内存
- 调用ExitBootServices(),把控制权交给操作系统
- UEFI运行时服务(Runtime Services)保留,供操作系统调用
RT(Runtime)阶段是UEFI的最终形态。操作系统已经接管,但UEFI的运行时服务仍然可用,比如:
- GetVariable() / SetVariable()——读写UEFI变量
- GetTime() / SetTime()——系统时间管理
- ResetSystem()——重启或关机
嗯,这里要注意:RT阶段的代码必须支持虚拟地址转换。操作系统可能会改变内存映射,UEFI运行时服务必须能适应这种变化。我见过一个第三方驱动在RT阶段直接访问物理地址,结果操作系统一开虚拟内存就崩溃了。
个人建议:调试RT阶段的问题特别痛苦,因为操作系统已经接管了硬件。我一般会在TSL阶段把关键日志写到NVRAM里,等系统重启后再读取分析。这个方法帮我解决过至少三个线上问题。