3、UEFI固件架构:SEC阶段、PEI阶段、DXE阶段、BDS阶段、TSL阶段、RT阶段、AL阶段
UEFI 的启动流程,说白了就是一台电脑从按下电源键到操作系统完全接管硬件的过程。整个流程被分成了七个阶段,每个阶段都有明确的分工。我刚开始接触 UEFI 时,觉得这七个阶段记起来很麻烦。后来做项目多了才发现,这种分阶段设计其实非常巧妙——每个阶段只做自己该做的事,出了问题也容易定位。
这七个阶段分别是:SEC、PEI、DXE、BDS、TSL、RT、AL。嗯,咱们一个一个来看。
核心观点:UEFI 的七个阶段是递进关系,前一个阶段为后一个阶段准备好执行环境。每个阶段都有明确的入口和出口,不能跳过,也不能倒序。
3.1 SEC 阶段 — 安全验证的起点
SEC(Security Phase)是 UEFI 的第一个阶段。CPU 复位后,第一条指令就落在 SEC 阶段。这个阶段的任务很纯粹:验证下一个阶段(PEI)的代码是否可信。
我个人习惯把 SEC 阶段理解为「信任链的根」。为什么这么说?因为 SEC 阶段运行在 CPU 最原始的复位向量处,此时内存都还没初始化。它只能使用 CPU 内部的缓存(Cache)作为临时存储,也就是所谓的 Cache-as-RAM(CAR)。
SEC 阶段主要做三件事:
- 建立临时内存(CAR) — 把 CPU 的缓存配置成内存来用,否则连栈都没有,代码没法跑。
- 验证 PEI 阶段的代码签名 — 用公钥检查 PEI 代码的哈希值,确保没被篡改。
- 传递启动参数 — 把 CPU 状态、系统配置等信息打包,交给 PEI 阶段。
避坑指南:我曾经在一个项目中遇到过 CAR 配置错误导致系统无法启动的问题。当时查了两天才发现,是 CPU 的 MTRR(Memory Type Range Register)设置不对,导致缓存区域重叠。嗯,从那以后我每次写 SEC 代码都会先检查 MTRR 配置。
3.2 PEI 阶段 — 内存初始化的关键
PEI(Pre-EFI Initialization Phase)阶段的任务很明确:把内存初始化好,为后续的 DXE 阶段铺路。PEI 阶段仍然运行在 CAR 上,但它的目标是把真正的 DRAM 初始化出来。
PEI 阶段的核心是 PEI 核心(PEI Core)和 PEIM(PEI Module)。PEI Core 负责调度 PEIM,PEIM 则是具体的功能模块。你想想看,内存初始化、CPU 微码更新、系统总线配置,这些都是由不同的 PEIM 完成的。
PEI 阶段有几个关键点:
- PEI 核心启动 — SEC 阶段跳转到 PEI Core 的入口,PEI Core 开始解析 HOB(Hand-Off Block)结构。
- PEIM 调度 — PEI Core 按依赖关系依次执行 PEIM,每个 PEIM 完成一个特定功能。
- 内存初始化 — 这是 PEI 阶段最重要的任务。内存控制器配置、时序训练、内存检测,都在这里完成。
- HOB 构建 — PEI 阶段把系统信息打包成 HOB,传递给 DXE 阶段。
注意:PEI 阶段的内存初始化非常敏感。我曾经见过一个案例,因为内存时序参数设置得太紧,导致系统在高温环境下频繁死机。后来放宽了时序参数,问题就解决了。所以,内存初始化时一定要留够余量。
3.3 DXE 阶段 — 驱动执行的舞台
DXE(Driver Execution Environment Phase)是 UEFI 最核心的阶段。此时内存已经可用,DXE 阶段可以加载和执行各种驱动程序。说白了,DXE 阶段就是 UEFI 的「操作系统」,它负责管理所有硬件资源。
DXE 阶段由 DXE 核心(DXE Core)和 DXE 驱动(DXE Driver)组成。DXE Core 负责调度驱动,DXE Driver 则提供具体的硬件抽象服务。
DXE 阶段的主要工作:
- DXE 核心启动 — 从 PEI 阶段接收 HOB,初始化 DXE 核心的服务。
- 驱动调度 — DXE Core 按依赖关系加载 DXE Driver,每个驱动注册自己的协议(Protocol)。
- 协议注册 — 驱动通过协议提供接口,其他驱动可以通过协议调用这些接口。
- 系统表构建 — DXE 阶段构建系统表(System Table)和启动服务表(Boot Services Table),供后续阶段使用。
| 阶段 | 内存状态 | 主要任务 | 输出 |
|---|---|---|---|
| SEC | CAR(Cache-as-RAM) | 安全验证、建立临时内存 | 启动参数、HOB 头部 |
| PEI | CAR → DRAM | 内存初始化、CPU 微码更新 | HOB 完整结构 |
| DXE | DRAM 可用 | 驱动加载、协议注册 | 系统表、启动服务表 |
| BDS | DRAM 可用 | 启动策略、设备枚举 | 启动设备列表 |
| TSL | DRAM 可用 | 操作系统加载器执行 | 操作系统内核 |
| RT | DRAM 可用 | 运行时服务 | 操作系统运行环境 |
| AL | DRAM 可用 | 灾难恢复 | 系统恢复 |
经验之谈:DXE 阶段的驱动调度顺序很重要。我建议在开发时,先确保基础驱动(如 PCI 枚举、中断控制器)加载成功,再加载功能驱动。否则,依赖关系没处理好,系统很容易卡在 DXE 阶段。
3.4 BDS 阶段 — 启动策略的执行者
BDS(Boot Device Selection Phase)阶段的任务是决定从哪个设备启动。DXE 阶段已经把硬件都初始化好了,BDS 阶段只需要做一件事:按照用户配置的启动顺序,尝试从每个设备加载操作系统。
BDS 阶段的核心是启动策略。我个人习惯把启动策略理解为「一个有序的设备列表」。系统会按顺序尝试每个设备,直到找到一个可以启动的设备为止。
BDS 阶段的主要工作:
- 读取启动配置 — 从 NVRAM 中读取启动顺序和启动选项。
- 设备枚举 — 枚举所有可启动设备,包括硬盘、U 盘、光驱、网络等。
- 启动尝试 — 按顺序尝试每个设备,直到成功。
- 启动管理器 — 如果用户按了 F12 或其他快捷键,显示启动菜单。
注意:BDS 阶段如果找不到可启动设备,系统会进入「等待状态」。我曾经遇到过一个问题:U 盘的启动顺序排在硬盘前面,但 U 盘里没有操作系统,结果系统每次都尝试从 U 盘启动,失败后才从硬盘启动,导致启动时间变长。后来我把 U 盘的启动顺序调到了硬盘后面,问题就解决了。
3.5 TSL 阶段 — 操作系统的加载器
TSL(Transient System Load Phase)阶段是 UEFI 和操作系统之间的桥梁。BDS 阶段找到了启动设备,TSL 阶段就负责从启动设备上加载操作系统加载器(如 Windows 的 bootmgfw.efi 或 Linux 的 GRUB)。
TSL 阶段的核心是操作系统加载器。加载器会读取文件系统,找到操作系统内核,然后跳转到内核入口。
TSL 阶段的主要工作:
- 加载操作系统加载器 — 从启动设备上读取 EFI 应用程序(如 bootmgfw.efi)。
- 执行加载器 — 加载器会读取配置文件,找到操作系统内核。
- 退出启动服务 — 加载器调用 ExitBootServices(),把控制权交给操作系统。
避坑指南:ExitBootServices() 是一个不可逆的操作。调用之后,UEFI 的启动服务就不可用了,只剩下运行时服务。我曾经见过一个案例,加载器在调用 ExitBootServices() 之后还试图调用启动服务,结果系统直接崩溃。所以,一定要确保在调用 ExitBootServices() 之前,所有启动服务都已经不再需要。
3.6 RT 阶段 — 运行时服务
RT(Runtime Phase)阶段是操作系统运行时的 UEFI 服务。操作系统启动后,UEFI 的大部分代码都被释放了,只保留了一小部分运行时服务。这些服务包括:
- 时间服务 — 读取和设置系统时间。
- 变量服务 — 读写 NVRAM 中的 UEFI 变量。
- 重置服务 — 重启或关机。
- 虚拟内存映射 — 支持操作系统对 UEFI 运行时服务的虚拟地址映射。
RT 阶段的特点是:代码和数据都驻留在内存中,操作系统可以通过 UEFI 运行时服务接口调用它们。嗯,这里要注意,运行时服务必须支持虚拟地址转换,因为操作系统会启用分页机制。
3.7 AL 阶段 — 最后的防线
AL(After Life Phase)阶段是 UEFI 的最后一个阶段。它只在系统发生灾难性错误时才会进入。比如,操作系统崩溃了,或者 UEFI 检测到硬件故障,系统会尝试进入 AL 阶段进行恢复。
AL 阶段的主要任务:
- 错误记录 — 把错误信息写入 NVRAM 或日志文件。
- 系统恢复 — 尝试重启系统,或者进入恢复模式。
- 硬件诊断 — 运行硬件自检,找出故障原因。
注意:AL 阶段不是所有 UEFI 实现都支持的。我建议在开发时,至少实现一个简单的错误记录功能,这样系统崩溃后还能找到原因。否则,出了问题只能靠猜。
3.8 七个阶段的整体流程
为了让你更直观地理解这七个阶段的关系,我画了一张流程图。嗯,这张图展示了从 SEC 到 AL 的完整流程,以及每个阶段的核心任务。
从图中可以看出,SEC → PEI → DXE → BDS → TSL → RT 是正常启动路径。AL 阶段是一个异常路径,只在系统崩溃时才会进入。嗯,这里要注意,AL 阶段不是所有 UEFI 实现都支持的,但如果你在做安全关键系统,我建议一定要实现它。
3.9 各阶段的内存使用情况
不同阶段对内存的使用方式完全不同。我整理了一张表,方便你对比:
| 阶段 | 内存类型 | 可用大小 | 特点 |
|---|---|---|---|
| SEC | Cache-as-RAM | 几 KB 到几十 KB | 速度极快,但容量极小 |
| PEI | CAR + 早期 DRAM | 几十 KB 到几 MB | 逐步过渡到 DRAM |
| DXE | DRAM | 全部可用内存 | 容量大,支持动态分配 |
| BDS | DRAM | 全部可用内存 | 与 DXE 共享内存空间 |
| TSL | DRAM | 全部可用内存 | 加载操作系统加载器 |
| RT | DRAM(保留部分) | 运行时服务占用 | 操作系统可访问 |
| AL | DRAM(保留部分) | 错误记录用 | 仅在崩溃时使用 |
经验之谈:SEC 和 PEI 阶段的内存非常宝贵。我建议在写这两个阶段的代码时,尽量少用全局变量,多用栈变量。否则,CAR 空间很容易被撑爆。我曾经见过一个项目,PEI 阶段的全局变量太多,导致 CAR 溢出,系统直接死机。后来把一些变量改成了动态分配,问题就解决了。
3.10 各阶段的代码示例
为了让你更直观地理解各阶段的代码风格,我给出一个简单的示例。嗯,这里以 PEI 阶段的一个 PEIM 为例:
// PEI 阶段的一个简单 PEIM 示例
#include <PiPei.h>
#include <PeiServicesLib.h>
EFI_STATUS
EFIAPI
SamplePeimEntry (
IN EFI_PEI_FILE_HANDLE FileHandle,
IN CONST EFI_PEI_SERVICES **PeiServices
)
{
EFI_STATUS Status;
UINT64 MemorySize;
// 获取内存大小
Status = (*PeiServices)->GetHobList (PeiServices, &MemorySize);
if (EFI_ERROR (Status)) {
return Status;
}
// 打印内存信息(实际项目中会使用 DebugLib)
// DEBUG ((DEBUG_INFO, "Memory Size: %ld\n", MemorySize));
return EFI_SUCCESS;
}
这段代码展示了 PEI 阶段 PEIM 的基本结构。入口函数接收两个参数:FileHandle 和 PeiServices。PeiServices 是一个指针,指向 PEI 服务表,通过它可以调用 PEI 阶段的各种服务。
注意:PEI 阶段的代码不能使用全局变量(除非放在 HOB 中),因为此时内存还没有完全初始化。我建议把所有状态信息都放在栈上,或者通过 HOB 传递。否则,很容易出现内存访问异常。
3.11 总结
UEFI 的七个阶段,每个阶段都有自己的使命。SEC 阶段建立信任链,PEI 阶段初始化内存,DXE 阶段加载驱动,BDS 阶段选择启动设备,TSL 阶段加载操作系统,RT 阶段提供运行时服务,AL 阶段处理灾难恢复。嗯,理解了这个流程,你就掌握了 UEFI 启动的核心逻辑。
我个人觉得,这七个阶段中最容易出问题的是 PEI 和 DXE 阶段。PEI 阶段的内存初始化很敏感,DXE 阶段的驱动调度很复杂。如果你在做 UEFI 开发,我建议先把这两个阶段吃透,其他的阶段自然就通了。