第二章 UEFI规范核心概念:启动流程、固件阶段与系统服务
UEFI(统一可扩展固件接口)取代传统BIOS,并非仅仅更换了启动界面,而是从根本上重构了固件与操作系统之间的接口契约。理解UEFI,必须从三个维度切入:启动流程(系统如何从上电过渡到OS)、固件阶段划分(各阶段职责与交接条件)、系统表与运行时服务(固件与OS在运行期的协作机制)。本章将围绕这三条主线,结合EDK II工程实践,建立UEFI开发的整体认知框架。
上图展示了UEFI固件从SEC阶段到运行时服务的完整架构。各阶段并非简单的线性执行,而是存在严格的数据交接(通过HOB)和能力降级(从启动服务到运行时服务)关系。下面逐一展开。
2.1 固件阶段划分:SEC/PEI/DXE/BDS/RT
核心原理:阶段职责与交接条件
UEFI固件将启动过程划分为五个阶段,每个阶段解决特定问题,并为下一阶段准备必要条件。这种分层设计使得固件可以在不同硬件平台上复用,同时保证安全性和可扩展性。
| 阶段 | 全称 | 核心职责 | 关键输出 | 典型代码位置 |
|---|---|---|---|---|
| SEC | Security | 上电后第一条指令执行,验证固件完整性,建立临时RAM(Cache as RAM) | 临时RAM可用、安全状态 | Reset Vector, SecCore |
| PEI | Pre-EFI Initialization | 初始化CPU、芯片组、内存控制器,发现并报告内存 | HOB列表(含内存描述) | PeiCore, PEIMs |
| DXE | Driver Execution Environment | 加载并执行DXE驱动,提供完整的UEFI协议服务 | EFI系统表、Boot Services | DxeCore, DXE Drivers |
| BDS | Boot Device Selection | 枚举启动设备,按BootOrder加载OS Loader | 启动项选择、OS Loader入口 | BdsDxe, Boot Manager |
| RT | Runtime | OS接管后,仅保留运行时服务(时间、变量、复位) | 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阶段内存管理完全可用。
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)。
- 背景约束:Reset Vector位于0xFFFFFFF0,只有16字节空间,需要立即跳转到SecCore。
- 初始现象:SecCore执行时,
esp寄存器不可用,任何函数调用都会导致崩溃。 - 关键操作:SecCore通过配置MTRR(Memory Type Range Registers)将特定Cache区域设置为Write-Back模式,然后填充该区域触发Cache填充,最后修改
esp指向该Cache区域,实现临时栈。 - 结果对比:CAR建立后,C语言函数调用、局部变量、函数返回地址均可正常工作,PEI阶段可以开始执行复杂逻辑。
- 风险代价:CAR容量有限(通常几百KB),且Cache行替换可能导致数据丢失。因此PEI阶段必须尽快初始化DRAM,并将HOB数据迁移到真实内存。
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 Vector | UefiCpuPkg/ResetVector | ResetVectorCode | ResetVector.efi |
| SEC | UefiCpuPkg/SecCore | SecStartup() | SecCore.efi |
| PEI | MdeModulePkg/Core/Pei | PeiCore() | PeiCore.efi |
| DXE | MdeModulePkg/Core/Dxe | DxeMain() | DxeCore.efi |
| BDS | MdeModulePkg/Universal/BdsDxe | BdsEntry() | 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的分水岭。
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运行期间用户可以修改密码。实现方案如下:
- 背景约束:密码需要跨重启保存,且OS运行期可读写。
- 初始设计:在DXE驱动中注册一个SetPassword协议,通过Boot Services的InstallProtocolInterface暴露给OS。
- 问题发现:OS Loader调用ExitBootServices后,Boot Services失效,OS无法再调用SetPassword协议。
- 解决方案:改用Runtime Services的SetVariable接口,将密码存储在名为"AdminPwd"的UEFI变量中,属性设为
EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS。 - 实现步骤:
// 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); } - 结果验证:OS中通过GetVariable读取密码,SetVariable修改密码,重启后密码仍然保留。
- 风险代价:NVRAM写入次数有限(通常10万次),频繁写入会磨损Flash。需要增加写入次数限制和磨损均衡策略。
常见误区与总结复盘
| 误区 | 正确理解 | 后果 |
|---|---|---|
| 认为PEI阶段可以调用gBS->AllocatePool | PEI阶段只能使用PeiServices,DXE阶段才有Boot Services | 编译错误或运行时崩溃 |
| 在ExitBootServices后继续使用Boot Services | 调用后Boot Services指针失效,仅Runtime Services可用 | 系统挂起或数据损坏 |
| 忽略SetVirtualAddressMap | 64位OS必须调用此服务重映射固件地址 | OS启动后固件服务崩溃 |
| 将UEFI变量属性设为仅BOOTSERVICE_ACCESS | OS运行期无法访问,只能启动时读取 | OS无法修改配置 |