第四章 UEFI启动流程详解:七个阶段与NVRAM
UEFI固件的启动,说白了就是一段从硬件复位到操作系统接管前的“接力赛”。我刚开始接触UEFI时,总觉得这七个阶段很抽象,直到有一次在调试一块国产板卡时,发现系统卡在DXE阶段死活过不去——嗯,从那以后我才真正理解了每个阶段存在的意义。
这一章,我们就把UEFI启动的七个阶段掰开揉碎,再聊聊UEFI变量和NVRAM这个“小仓库”。
4.1 UEFI启动的七个阶段
UEFI规范将固件启动划分为七个阶段:SEC、PI、DXE、BDS、TSL、RT、AL。每个阶段都有明确的职责和边界。我个人习惯把这七个阶段分成三组来看:
- 硬件初始化组:SEC → PI → DXE(搞定CPU、芯片组、内存)
- 策略决策组:BDS → TSL(选择启动设备、加载启动项)
- 运行时组:RT → AL(交出控制权、最终关机)
核心观点:七个阶段不是简单的顺序执行,而是层层递进的“能力积累”。前一个阶段为后一个阶段准备好必要的资源。
下面这张图可以帮你快速建立整体认知:
4.2 各阶段深度解析
4.2.1 SEC阶段(安全验证阶段)
SEC是系统复位后执行的第一个代码块。它运行在缓存作为内存(CAR)的环境中——因为此时内存控制器还没初始化。SEC的主要任务有三件:
- 验证下一个阶段(PI)的代码完整性
- 建立临时内存(Cache-As-RAM)
- 传递系统状态信息到PI阶段
避坑指南:我曾经在调试一块服务器主板时,发现SEC阶段总是报校验错误。查了两天才发现,是SPI Flash上的PI镜像被意外修改了。所以,SEC阶段的验证逻辑一定要保留,别为了省时间把它跳过。
4.2.2 PI阶段(CPU/芯片组初始化)
PI阶段,全称是Pre-EFI Initialization。它负责初始化CPU、芯片组和主板的基本功能。这个阶段会做:
- CPU微码加载
- 内存控制器初始化
- 基本I/O空间配置
- 建立HOB(Hand-Off Block)传递给DXE
你想想看,PI阶段结束后,系统才真正有了“内存”这个概念。之前SEC阶段用的CAR,说白了就是拿CPU缓存临时凑合一下。
4.2.3 DXE阶段(驱动执行环境)
DXE是UEFI固件中最庞大的阶段。它负责加载和执行各种UEFI驱动——磁盘驱动、网络驱动、显示驱动等等。DXE阶段的核心数据结构是Handle Database和Protocol。
我个人习惯把DXE比作“乐高积木搭建过程”:每个驱动是一块积木,通过Protocol互相连接。驱动加载顺序由依赖关系决定,而不是简单的顺序执行。
关键点:DXE阶段结束后,系统已经具备了完整的硬件抽象层(HAL)。所有硬件设备都有了对应的UEFI驱动。
4.2.4 BDS阶段(启动设备选择)
BDS阶段负责决定“从哪个设备启动”。它会读取BootOrder变量,依次尝试每个启动项。如果第一个启动项失败,就尝试第二个,以此类推。
BDS阶段还会显示UEFI启动菜单(如果有的话)。我记得有一次,客户反馈说系统启动时总是跳过UEFI菜单直接进系统。查下来发现是BDS阶段的Timeout变量被设成了0——嗯,这个坑后来成了我们培训材料里的经典案例。
4.2.5 TSL阶段(临时系统加载)
TSL阶段是操作系统加载器(如bootmgfw.efi)运行的阶段。它负责:
- 加载操作系统内核
- 准备启动参数
- 调用ExitBootServices()服务
这里有个重要概念:ExitBootServices()是UEFI固件交出控制权的分水岭。调用之后,UEFI运行时服务仍然可用,但启动服务全部失效。
4.2.6 RT阶段(运行时服务)
RT阶段,操作系统已经接管了硬件。UEFI固件退居二线,只提供运行时服务:
- UEFI变量读写
- 实时时钟(RTC)
- 重置系统
这些服务在操作系统运行期间一直可用,直到系统关机。
4.2.7 AL阶段(最终阶段)
AL阶段是系统关机或重启前的最后一步。它负责清理资源、保存关键变量到NVRAM。这个阶段通常很短,但很重要——如果AL阶段崩溃,可能导致NVRAM数据损坏。
4.3 UEFI变量与NVRAM
UEFI变量是固件和操作系统之间交换信息的键值对存储。它们保存在NVRAM(非易失性随机访问存储器)中,系统断电后数据不丢失。
4.3.1 UEFI变量的属性
| 属性 | 说明 | 典型用途 |
|---|---|---|
| BootServiceAccess | 仅在启动服务阶段可访问 | 临时配置数据 |
| RuntimeAccess | 操作系统运行时也可访问 | 启动顺序、安全启动状态 |
| NonVolatile | 断电后数据保留 | 所有持久化配置 |
| TimeBasedAuthenticatedWrite | 需要数字签名才能写入 | 安全启动变量(如db、dbx) |
警告:我曾经见过一个案例,开发人员误删了SecureBoot变量,导致系统无法启动。修复方法是用编程器重新烧录NVRAM内容。所以,操作UEFI变量时一定要小心,尤其是带TimeBasedAuthenticatedWrite属性的变量。
4.3.2 常用UEFI变量
以下是我在项目中经常打交道的几个变量:
- BootOrder:启动设备顺序列表
- Boot####:单个启动项配置(####是十六进制编号)
- SecureBoot:安全启动开关状态
- db:允许签名的证书/哈希数据库
- dbx:禁止签名的证书/哈希数据库
- KEK:密钥交换密钥,用于管理db/dbx更新
- PK:平台密钥,最高权限密钥
4.3.3 读写UEFI变量的代码示例
下面是一个简单的UEFI应用程序,演示如何读取和设置变量:
#include <Uefi.h>
#include <Library/UefiRuntimeServicesTableLib.h>
EFI_STATUS
ReadBootOrder (
VOID
)
{
EFI_STATUS Status;
UINT16 *BootOrder;
UINTN DataSize = 0;
// 第一次调用获取数据大小
Status = gRT->GetVariable (
L"BootOrder",
&gEfiGlobalVariableGuid,
NULL,
&DataSize,
NULL
);
if (Status != EFI_BUFFER_TOO_SMALL) {
return Status;
}
BootOrder = AllocatePool (DataSize);
if (BootOrder == NULL) {
return EFI_OUT_OF_RESOURCES;
}
// 第二次调用读取实际数据
Status = gRT->GetVariable (
L"BootOrder",
&gEfiGlobalVariableGuid,
NULL,
&DataSize,
BootOrder
);
if (EFI_ERROR (Status)) {
FreePool (BootOrder);
return Status;
}
// 打印启动顺序
UINTN Index;
for (Index = 0; Index < DataSize / sizeof (UINT16); Index++) {
Print (L"Boot%04x\n", BootOrder[Index]);
}
FreePool (BootOrder);
return EFI_SUCCESS;
}
提示:读取UEFI变量时,一定要先调用一次GetVariable获取实际数据大小,再分配缓冲区。这是UEFI编程的常见模式,我刚开始写时经常忘记这一步,导致缓冲区溢出。
4.4 七个阶段与NVRAM的交互
NVRAM在七个阶段中扮演着“记忆体”的角色。不同阶段对NVRAM的访问权限不同:
| 阶段 | NVRAM访问权限 | 典型操作 |
|---|---|---|
| SEC | 只读(受限) | 读取安全启动策略 |
| PI | 只读 | 读取硬件配置参数 |
| DXE | 读写 | 注册驱动、更新配置 |
| BDS | 读写 | 读取BootOrder、更新启动项 |
| TSL | 读写 | OS Loader读取启动参数 |
| RT | 读写(运行时服务) | 操作系统更新安全启动变量 |
| AL | 只写 | 保存最终状态 |
嗯,这里要注意:SEC和PI阶段对NVRAM的访问是受限的。因为此时SPI控制器可能还没完全初始化,直接读写NVRAM有风险。我见过一些固件在SEC阶段强行写NVRAM,结果导致SPI Flash时序错乱,整块板子变砖。
4.5 本章小结
UEFI的七个阶段,说白了就是一条从“裸金属”到“操作系统”的进化链。每个阶段都有明确的职责和边界。NVRAM则是贯穿始终的“记忆体”,保存着系统配置和安全策略。
我个人觉得,理解这七个阶段的关键不在于记住每个阶段的名字,而在于理解阶段之间的依赖关系和交接机制。比如,SEC阶段为什么需要CAR?因为内存还没初始化。DXE阶段为什么需要Protocol?因为驱动之间需要解耦。这些设计背后的逻辑,才是UEFI架构的精髓。