UEFI固件架构:从SEC到RT的完整旅程

UEFI固件的启动过程,说白了就是一台电脑从按下电源键到操作系统接管前的全部秘密。我做了这么多年固件,见过太多工程师只盯着自己那一亩三分地——写PEI的不知道DXE在干嘛,做BDS的看不懂SEC的代码。这样不行。

今天我把UEFI的六个阶段掰开揉碎了讲给你听。每个阶段做什么、怎么衔接、有哪些坑,咱们一次说清楚。

核心要点:UEFI启动分为SEC→PEI→DXE→BDS→TSL→RT六个阶段,每个阶段都有明确的职责边界和交接协议。理解这个架构,是做好固件开发和安全启动的基础。

UEFI 启动阶段总览 SEC 安全验证 PEI 早期初始化 DXE 驱动执行 BDS 启动选择 TSL 操作系统加载 RT 运行时 关键说明: • SEC→PEI→DXE→BDS 是启动前阶段,TSL是操作系统加载阶段,RT是运行时阶段 • 每个阶段都有独立的HOB/Protocol传递机制,数据不能跨阶段乱用 • 安全启动的签名验证在SEC和DXE阶段都有介入点,后面会详细讲 • 我见过有人把PEI的代码写到DXE里跑,结果系统直接挂掉——阶段边界不能乱跨

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,它负责:

  1. 扫描所有固件卷(FV),找到DXE驱动
  2. 检查驱动的依赖关系(DEPEX)
  3. 按依赖顺序加载并执行驱动
  4. 驱动注册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里,等系统重启后再读取分析。这个方法帮我解决过至少三个线上问题。