第二章 UEFI规范核心概念:启动流程、固件阶段与系统服务

UEFI(统一可扩展固件接口)取代传统BIOS,并非仅仅更换了启动界面,而是从根本上重构了固件与操作系统之间的接口契约。理解UEFI,必须从三个维度切入:启动流程(系统如何从上电过渡到OS)、固件阶段划分(各阶段职责与交接条件)、系统表与运行时服务(固件与OS在运行期的协作机制)。本章将围绕这三条主线,结合EDK II工程实践,建立UEFI开发的整体认知框架。

UEFI 固件阶段与系统服务架构 SEC 安全验证/临时RAM Reset Vector → CAR PEI CPU/芯片组/内存初始化 PEI Core + PEIMs DXE 驱动执行环境 DXE Core + Drivers BDS 启动设备选择 Boot Manager RT (Runtime) OS 运行期服务 EFI 系统表 (gST) EFI_BOOT_SERVICES 启动服务表 EFI_RUNTIME_SERVICES 运行时服务表 EFI_CONFIG_TABLE 配置表数组 EFI_SYSTEM_TABLE 头/固件版本 运行时服务关键接口 GetTime/SetTime 实时时钟访问 GetVariable/SetVariable NVRAM 变量读写 ResetSystem 系统复位控制 OS Loader 加载 OS 内核 ExitBootServices() 关键交接点: ① SEC→PEI: 临时RAM建立 (CAR) ② PEI→DXE: 内存描述符HOB传递 ③ DXE→BDS: 启动设备枚举完成 ④ BDS→RT: ExitBootServices() 后启动服务失效,仅保留运行时服务 ⑤ 系统表指针通过寄存器传递 (IA32: EBX/EDX)

上图展示了UEFI固件从SEC阶段到运行时服务的完整架构。各阶段并非简单的线性执行,而是存在严格的数据交接(通过HOB)和能力降级(从启动服务到运行时服务)关系。下面逐一展开。

2.1 固件阶段划分:SEC/PEI/DXE/BDS/RT

核心原理:阶段职责与交接条件

UEFI固件将启动过程划分为五个阶段,每个阶段解决特定问题,并为下一阶段准备必要条件。这种分层设计使得固件可以在不同硬件平台上复用,同时保证安全性和可扩展性。

阶段全称核心职责关键输出典型代码位置
SECSecurity上电后第一条指令执行,验证固件完整性,建立临时RAM(Cache as RAM)临时RAM可用、安全状态Reset Vector, SecCore
PEIPre-EFI Initialization初始化CPU、芯片组、内存控制器,发现并报告内存HOB列表(含内存描述)PeiCore, PEIMs
DXEDriver Execution Environment加载并执行DXE驱动,提供完整的UEFI协议服务EFI系统表、Boot ServicesDxeCore, DXE Drivers
BDSBoot Device Selection枚举启动设备,按BootOrder加载OS Loader启动项选择、OS Loader入口BdsDxe, Boot Manager
RTRuntimeOS接管后,仅保留运行时服务(时间、变量、复位)Runtime Services表RuntimeDxe

判断方法:如何识别当前阶段

在EDK II调试中,可以通过以下方式判断固件当前所处阶段:

  • 调试日志前缀:SEC阶段通常输出"SecCore"或"ResetVector"日志;PEI阶段输出"PeiCore";DXE阶段输出"DxeCore"。
  • 可用服务:PEI阶段只能使用PEI Services(如PeiServices->GetHobList),DXE阶段才能使用gBS->AllocatePool等Boot Services。
  • 内存状态:PEI阶段早期无内存,只能使用临时RAM;DXE阶段内存管理完全可用。
实战经验:在EDK II中,可以通过 DEBUG_INIT 宏区分阶段。例如在PEIM中使用 DEBUG ((DEBUG_INIT, "PEI Phase\n")),在DXE驱动中使用 DEBUG ((DEBUG_INIT, "DXE Phase\n"))。日志级别和前缀能快速定位代码执行阶段。

案例拆解:从SEC到PEI的临时RAM交接

以x86平台为例,SEC阶段的核心挑战是:上电时DRAM尚未初始化,代码执行需要栈和变量存储空间。解决方案是利用CPU的Cache作为临时RAM(Cache as RAM, CAR)。

  1. 背景约束:Reset Vector位于0xFFFFFFF0,只有16字节空间,需要立即跳转到SecCore。
  2. 初始现象:SecCore执行时,esp寄存器不可用,任何函数调用都会导致崩溃。
  3. 关键操作:SecCore通过配置MTRR(Memory Type Range Registers)将特定Cache区域设置为Write-Back模式,然后填充该区域触发Cache填充,最后修改esp指向该Cache区域,实现临时栈。
  4. 结果对比:CAR建立后,C语言函数调用、局部变量、函数返回地址均可正常工作,PEI阶段可以开始执行复杂逻辑。
  5. 风险代价:CAR容量有限(通常几百KB),且Cache行替换可能导致数据丢失。因此PEI阶段必须尽快初始化DRAM,并将HOB数据迁移到真实内存。
常见误区:初学者常误以为PEI阶段可以直接使用 AllocatePool 分配内存。实际上,PEI阶段的内存分配只能通过 PeiServices->AllocatePages 从内存描述符HOB中分配,且分配后不能释放。DXE阶段才提供完整的堆/栈管理。

2.2 UEFI启动流程:从Reset Vector到OS Loader

核心原理:启动流程的完整链路

UEFI启动流程是一个状态机,每个阶段有明确的入口和出口条件。理解启动流程,关键在于掌握各阶段之间的数据交接和控制权转移机制。

上电 → Reset Vector (0xFFFFFFF0)
  → SEC: 安全验证、CAR建立
  → PEI: 内存初始化、HOB创建
  → DXE: 驱动加载、协议注册
  → BDS: 启动项选择、OS Loader加载
  → OS Loader: 调用 ExitBootServices()
  → RT: 仅保留运行时服务,OS接管

操作流程:EDK II中启动流程的代码映射

在EDK II工程中,启动流程对应以下关键模块:

流程节点EDK II模块关键函数/入口编译产物
Reset VectorUefiCpuPkg/ResetVectorResetVectorCodeResetVector.efi
SECUefiCpuPkg/SecCoreSecStartup()SecCore.efi
PEIMdeModulePkg/Core/PeiPeiCore()PeiCore.efi
DXEMdeModulePkg/Core/DxeDxeMain()DxeCore.efi
BDSMdeModulePkg/Universal/BdsDxeBdsEntry()BdsDxe.efi

案例拆解:一次典型的UEFI启动日志分析

以下是从EDK II平台抓取的启动日志片段(已简化),展示各阶段交接点:

SecCore: Entering SEC
  - CAR setup: MTRR configured, cache as RAM at 0xFEF00000
PeiCore: PEI Entry
  - Memory Discovered: 0x10000000 - 0x7FFFFFFF (2GB)
  - HOB list at 0x1000000
DxeCore: DXE Entry
  - Boot Services Table at 0x7F8E0000
  - Runtime Services Table at 0x7F8D8000
BdsDxe: BDS Entry
  - BootOrder: 0000, 0001
  - Loading OS Loader at \EFI\BOOT\BOOTX64.EFI
OS Loader: ExitBootServices() called
  - Runtime Services remain: GetTime, SetVariable, ResetSystem

通过日志可以清晰看到:HOB列表是PEI到DXE的核心交接物,系统表指针是DXE到BDS的交接物,ExitBootServices是固件到OS的分水岭。

关键理解:ExitBootServices() 是UEFI启动流程中最重要的一次"能力降级"。调用后,Boot Services(如AllocatePool、CreateEvent)全部失效,固件仅保留Runtime Services。OS Loader必须在调用前完成所有需要固件服务的操作。

2.3 UEFI系统表与运行时服务

核心原理:系统表的结构与作用

EFI系统表(EFI_SYSTEM_TABLE)是UEFI固件向OS Loader和应用程序暴露的唯一入口。它包含固件版本信息、控制台句柄、启动服务表指针、运行时服务表指针以及配置表数组。系统表指针通过CPU寄存器传递(IA32为EBX,X64为RDX),OS Loader在入口处接收。

typedef struct {
  EFI_TABLE_HEADER          Hdr;              // 签名 "IBI SYST"
  CHAR16                    *FirmwareVendor;  // 固件厂商字符串
  UINT32                    FirmwareRevision; // 固件版本
  EFI_HANDLE                ConsoleInHandle;  // 控制台输入句柄
  EFI_SIMPLE_TEXT_INPUT_PROTOCOL   *ConIn;     // 文本输入协议
  EFI_HANDLE                ConsoleOutHandle; // 控制台输出句柄
  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *ConOut;    // 文本输出协议
  EFI_HANDLE                StandardErrorHandle;
  EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL  *StdErr;
  EFI_RUNTIME_SERVICES      *RuntimeServices; // 运行时服务表
  EFI_BOOT_SERVICES         *BootServices;    // 启动服务表
  UINTN                     NumberOfTableEntries;
  EFI_CONFIGURATION_TABLE   *ConfigurationTable; // 配置表(ACPI, SMBIOS等)
} EFI_SYSTEM_TABLE;

运行时服务:OS运行期的固件接口

运行时服务是OS运行期间唯一可用的固件服务,包括:

  • 时间服务:GetTime/SetTime/GetWakeupTime/SetWakeupTime,用于RTC访问。
  • 变量服务:GetVariable/GetNextVariableName/SetVariable/QueryVariableInfo,用于NVRAM读写。
  • 复位服务:ResetSystem,支持冷复位、热复位、关机等。
  • 其他:GetNextHighMonotonicCount(单调计数器)、SetVirtualAddressMap(虚拟地址映射,用于64位OS启用虚拟内存后重映射固件代码)。

案例拆解:通过运行时服务实现固件配置持久化

假设需要实现一个"开机密码"功能,密码存储在NVRAM中,OS运行期间用户可以修改密码。实现方案如下:

  1. 背景约束:密码需要跨重启保存,且OS运行期可读写。
  2. 初始设计:在DXE驱动中注册一个SetPassword协议,通过Boot Services的InstallProtocolInterface暴露给OS。
  3. 问题发现:OS Loader调用ExitBootServices后,Boot Services失效,OS无法再调用SetPassword协议。
  4. 解决方案:改用Runtime Services的SetVariable接口,将密码存储在名为"AdminPwd"的UEFI变量中,属性设为 EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS。
  5. 实现步骤:
    // DXE驱动中初始化变量
    STATIC EFI_GUID mAdminPwdGuid = {0x...};
    VOID InitAdminPwd(VOID) {
      CHAR16 *Pwd = L"123456";
      gRT->SetVariable(L"AdminPwd", &mAdminPwdGuid,
        EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS,
        (wcslen(Pwd)+1)*sizeof(CHAR16), Pwd);
    }
  6. 结果验证:OS中通过GetVariable读取密码,SetVariable修改密码,重启后密码仍然保留。
  7. 风险代价:NVRAM写入次数有限(通常10万次),频繁写入会磨损Flash。需要增加写入次数限制和磨损均衡策略。
重要提醒:运行时服务必须在SetVirtualAddressMap之后使用虚拟地址调用。在64位OS启用分页后,固件代码和数据会被映射到虚拟地址空间。如果OS未调用SetVirtualAddressMap,运行时服务只能使用物理地址,这在现代OS中几乎不可行。

常见误区与总结复盘

误区正确理解后果
认为PEI阶段可以调用gBS->AllocatePoolPEI阶段只能使用PeiServices,DXE阶段才有Boot Services编译错误或运行时崩溃
在ExitBootServices后继续使用Boot Services调用后Boot Services指针失效,仅Runtime Services可用系统挂起或数据损坏
忽略SetVirtualAddressMap64位OS必须调用此服务重映射固件地址OS启动后固件服务崩溃
将UEFI变量属性设为仅BOOTSERVICE_ACCESSOS运行期无法访问,只能启动时读取OS无法修改配置