第四章 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(交出控制权、最终关机)

核心观点:七个阶段不是简单的顺序执行,而是层层递进的“能力积累”。前一个阶段为后一个阶段准备好必要的资源。

下面这张图可以帮你快速建立整体认知:

UEFI启动七阶段流程图 UEFI 启动七阶段流程图 SEC 安全验证 PI CPU/芯片组初始化 DXE 驱动执行环境 BDS 启动设备选择 TSL 临时系统加载 RT 运行时服务 AL 最终阶段 关键说明 SEC→PI→DXE:硬件初始化;BDS→TSL:策略决策;RT→AL:运行时与最终阶段 硬件初始化组 策略决策组 运行时组 复位 OS接管 时间 →

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架构的精髓。