2、UEFI基础概念:UEFI规范体系、固件启动流程(SEC、PEI、DXE、BDS、RT)、UEFI驱动模型、UEFI运行时服务与启动服务

UEFI(Unified Extensible Firmware Interface,统一可扩展固件接口)取代传统Legacy BIOS,是连接操作系统与硬件固件之间的关键桥梁。理解UEFI规范体系、启动流程、驱动模型以及服务接口,是从事EDK2固件开发的基础。本章将从规范分层、启动阶段、驱动架构和服务接口四个维度,系统梳理UEFI的核心概念,并结合EDK2开源框架说明其工程落地方式。

UEFI固件启动流程与驱动模型总览 SEC 安全验证 PEI 内存初始化 DXE 驱动执行环境 BDS 启动设备选择 RT 运行时服务 OS 操作系统 UEFI驱动模型 Driver Binding Device Path I/O Abstraction Handle Database UEFI服务接口 Boot Services (BS) Runtime Services (RT) Protocol Interface Variable Services EDK2框架实现 MdePkg | MdeModulePkg | PcAtChipsetPkg | IntelSiliconPkg | 平台定制Pkg ● 红色虚线:电源/时钟域 ● 蓝色实线:数据流 ● 绿色区域:驱动模型 ● 黄色区域:服务接口 SEC→PEI→DXE→BDS→RT 为固件启动必经阶段,驱动模型与服务接口贯穿DXE及后续阶段

上图展示了UEFI固件从SEC到RT的完整启动链路,以及驱动模型和服务接口在整个体系中的位置。下面将逐一展开每个核心概念。

2.1 UEFI规范体系

核心原理

UEFI规范由UEFI论坛维护,当前主流版本为2.10。规范定义了固件与操作系统之间的接口标准,包括平台初始化、启动管理、运行时服务、驱动模型、协议定义等。与Legacy BIOS的16位实模式中断调用不同,UEFI运行在32位或64位保护模式下,使用C语言风格的接口定义,支持网络启动、大容量磁盘(分区)、安全启动(Secure Boot)等现代特性。

UEFI规范体系从逻辑上分为三个层次:平台初始化层(负责从复位向量到DXE阶段的硬件初始化)、固件接口层(提供Boot Services和Runtime Services)、协议层(定义驱动与设备之间的标准接口)。EDK2是Intel主导的开源UEFI实现,其包结构(MdePkg、MdeModulePkg等)严格对应规范层次。

规范层次对应EDK2包核心内容典型模块
平台初始化PcAtChipsetPkg, IntelSiliconPkgSEC/PEI/DXE阶段硬件初始化SecCore, PeiCore, DxeCore
固件接口MdePkgBoot Services, Runtime ServicesgBS, gRT, gST
协议层MdeModulePkg设备驱动协议、I/O抽象DevicePath, BlockIo, SimpleFileSystem
平台应用OemPkg(定制)BDS策略、平台专属功能PlatformBds, SetupBrowser

判断方法

在EDK2开发中,判断一个功能应放在哪个层次,可遵循以下原则:硬件寄存器操作放在平台初始化层;通用固件服务(如内存分配、事件处理)使用Boot Services;设备访问抽象通过Protocol实现;启动策略(如启动顺序、超时时间)放在BDS阶段。例如,要新增一个I2C控制器驱动,应实现EFI_DRIVER_BINDING_PROTOCOL,并注册到Handle Database,而不是直接修改DxeCore。

经验提醒:初学者常混淆Protocol和PPI(PEIM-to-PEIM Interface)。PPI用于PEI阶段,内存不可用,只能通过临时RAM通信;Protocol用于DXE阶段,内存可用,支持更复杂的驱动模型。在EDK2中,PPI定义在MdePkg/Include/Ppi目录,Protocol定义在MdePkg/Include/Protocol目录。

案例拆解

某项目需要在固件中支持板载TPM2.0芯片。按照规范体系,正确做法是:在PEI阶段通过PPI检测TPM是否存在(使用gPeiTpm2PpiGuid);在DXE阶段实现TPM驱动,注册EFI_TPM2_PROTOCOL;在BDS阶段根据TPM状态决定是否启用Secure Boot。如果直接在SEC阶段操作TPM寄存器,会因内存未初始化而失败。

常见误区

  • 误区1:认为UEFI规范只包含启动流程,忽略了协议定义和驱动模型的重要性。
  • 误区2:在PEI阶段使用Protocol,导致编译错误或运行时崩溃。
  • 误区3:自定义Protocol时未遵循UEFI命名规范(GUID格式),导致与其他模块冲突。

2.2 固件启动流程(SEC、PEI、DXE、BDS、RT)

核心原理

UEFI固件启动分为五个阶段,每个阶段有明确的目标和边界:

  1. SEC(Security Phase):CPU复位后的第一个阶段,负责安全验证、临时RAM(Cache-as-RAM)设置、传递系统参数。代码量最小,通常用汇编编写。
  2. PEI(Pre-EFI Initialization):初始化内存控制器,发现内存,将临时RAM切换到真实内存。PEIM(PEI Module)通过PPI通信,执行顺序由依赖关系决定。
  3. DXE(Driver Execution Environment):内存可用后,执行大部分驱动初始化。DXE Dispatcher按依赖关系加载驱动,注册Protocol,建立Handle Database。
  4. BDS(Boot Device Selection):根据启动策略选择启动设备(硬盘、U盘、网络等),加载启动项。BDS阶段是平台定制的重点。
  5. RT(Runtime):固件将控制权交给OS,但保留Runtime Services(如变量服务、时间服务)供OS调用。RT阶段代码必须常驻内存,且不能使用Boot Services。

操作流程

在EDK2中,各阶段的入口函数和构建方式如下:

// SEC阶段入口(汇编)
SecEntryPoint:
    ; 设置临时RAM
    ; 跳转到C代码
    call SecStartup

// PEI阶段入口(C语言)
EFI_STATUS
EFIAPI
PeiCoreEntryPoint (
    IN EFI_FFS_FILE_HEADER  *FfsHeader,
    IN EFI_PEI_SERVICES     **PeiServices
    );

// DXE阶段入口
EFI_STATUS
EFIAPI
DxeCoreEntryPoint (
    IN EFI_HANDLE           ImageHandle,
    IN EFI_SYSTEM_TABLE     *SystemTable
    );

// BDS阶段入口
EFI_STATUS
EFIAPI
BdsEntry (
    IN EFI_BDS_ARCH_PROTOCOL  *This
    );

案例拆解

某服务器平台在启动时出现内存初始化失败,错误代码显示在PEI阶段。排查过程:首先在PEI阶段添加调试打印(DEBUG ((DEBUG_INFO, "MemoryInit...\n"))),确认内存控制器寄存器配置是否正确;然后检查内存条是否插满,使用MemTestPEIM进行快速测试;最终发现是内存参考代码(MRC)参数配置错误,导致DDR5训练失败。修复后,在PEI阶段增加内存参数校验逻辑,避免类似问题。

常见误区

  • 误区1:在SEC阶段使用全局变量,但此时栈尚未建立,导致数据损坏。
  • 误区2:PEI阶段分配内存使用AllocatePages,但未考虑内存类型(EfiReservedMemoryType),导致后续阶段无法访问。
  • 误区3:BDS阶段直接调用Boot Services的ExitBootServices,但未正确处理内存映射,导致OS启动失败。
风险提示:RT阶段代码必须使用EfiRuntimeServicesCode和EfiRuntimeServicesData内存类型,否则OS无法访问。在EDK2中,可通过EFI_RUNTIME_SERVICES结构体中的ConvertPointer处理虚拟地址转换。

2.3 UEFI驱动模型

核心原理

UEFI驱动模型基于Driver Binding Protocol,实现驱动与设备的动态绑定。核心思想是:驱动不直接操作硬件,而是通过Protocol抽象设备功能。驱动模型包含三个关键组件:

  • EFI_DRIVER_BINDING_PROTOCOL:包含Supported、Start、Stop三个函数,分别用于检测设备兼容性、启动驱动、停止驱动。
  • Handle Database:系统维护的句柄列表,每个句柄关联多个Protocol。
  • Device Path Protocol:描述设备在系统中的位置,用于设备识别和启动项管理。

操作流程

驱动加载的典型流程:

  1. DXE Dispatcher加载驱动镜像,调用驱动入口函数。
  2. 驱动入口函数安装EFI_DRIVER_BINDING_PROTOCOL到新的Handle。
  3. 系统调用驱动的Supported()函数,检查设备是否支持。
  4. 若支持,调用Start()函数,驱动创建子Handle并安装I/O Protocol。
  5. 设备移除时,调用Stop()函数释放资源。

案例拆解

以NVMe驱动为例:NvmExpressDxe驱动安装Driver Binding Protocol后,系统遍历PCIe总线上的设备,调用Supported()检查设备的Class Code是否为NVMe(Class=01, Subclass=08, ProgIf=02)。匹配成功后,Start()函数创建Block I/O Protocol,供文件系统驱动使用。如果NVMe设备固件异常,Supported()可能返回EFI_UNSUPPORTED,此时需要检查设备是否被正确枚举。

常见误区

  • 误区1:驱动入口函数中直接操作硬件,而不是通过Driver Binding Protocol的Start函数。
  • 误区2:多个驱动竞争同一设备时,未正确处理优先级(Driver Binding Protocol的Version字段)。
  • 误区3:Stop函数未释放所有资源,导致内存泄漏或句柄残留。

2.4 UEFI运行时服务与启动服务

核心原理

UEFI服务分为两类:Boot Services(BS)和Runtime Services(RT)。BS在OS加载前可用,包括内存分配、事件处理、Protocol管理、镜像加载等;RT在OS运行期间仍可用,包括变量服务、时间服务、复位服务等。系统表(EFI_SYSTEM_TABLE)中的BootServices和RuntimeServices指针分别指向这两组服务。

服务类型典型接口使用场景生命周期
Boot ServicesAllocatePages, CreateEvent, InstallProtocolInterface驱动初始化、内存分配固件启动到ExitBootServices
Runtime ServicesGetVariable, SetVariable, GetTime, ResetSystemOS运行时访问固件数据固件启动到OS关机

操作流程

在EDK2中,使用Boot Services的典型代码:

// 分配内存
EFI_PHYSICAL_ADDRESS  Address;
Status = gBS->AllocatePages (
    AllocateAnyPages,
    EfiBootServicesData,
    Pages,
    &Address
    );

// 创建事件
EFI_EVENT  TimerEvent;
Status = gBS->CreateEvent (
    EVT_TIMER | EVT_NOTIFY_SIGNAL,
    TPL_CALLBACK,
    TimerCallback,
    NULL,
    &TimerEvent
    );
Status = gBS->SetTimer (TimerEvent, TimerPeriodic, 10000000); // 1秒

案例拆解

某项目需要在OS运行时读取固件配置。正确做法是使用Runtime Services的GetVariable接口。在EDK2中,变量存储在Flash的Firmware Volume中,通过SetVariable写入。注意:变量写入有大小限制(通常8KB),且需要遵循UEFI变量命名规范(GUID+名称)。如果OS无法读取变量,检查是否在ExitBootServices后仍使用Boot Services,或者变量属性设置错误(如未设置EFI_VARIABLE_NON_VOLATILE)。

常见误区

  • 误区1:在Runtime阶段调用Boot Services,导致系统崩溃。
  • 误区2:变量服务使用未加锁的全局缓冲区,导致并发访问冲突。
  • 误区3:时间服务未处理时区问题,导致OS显示时间错误。
总结复盘:UEFI体系的核心在于“分层”和“抽象”。启动流程的五个阶段各司其职,驱动模型通过Protocol实现设备无关性,服务接口则区分了固件生命周期和OS生命周期。在EDK2开发中,务必遵循规范定义,避免跨阶段调用服务。建议开发者从MdePkg的UefiSpec.h开始阅读,理解每个Protocol和Service的定义,再结合平台代码实践。