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 的完整流程,以及每个阶段的核心任务。

UEFI 七个阶段流程图 UEFI 七个阶段流程图 SEC PEI DXE BDS TSL RT AL 各阶段核心任务: • SEC:安全验证,建立 Cache-as-RAM • PEI:内存初始化,构建 HOB • DXE:驱动加载,协议注册 • BDS:启动策略,设备枚举 • TSL:操作系统加载器执行 • RT:运行时服务(时间、变量、重置) • 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 开发,我建议先把这两个阶段吃透,其他的阶段自然就通了。