UEFI基础架构:UEFI规范体系、运行时与启动时服务、驱动模型、系统表与句柄数据库
UEFI(Unified Extensible Firmware Interface)并非一个单一的程序,而是一整套定义了操作系统与平台固件之间接口的规范体系。理解UEFI的基础架构,是深入掌握固件卷(FV)与EFI启动文件系统(ESP)的基石。本章将围绕UEFI规范体系的分层结构、启动时(Boot Time)与运行时(Runtime)服务的本质区别、驱动模型(Driver Model)的工作机制,以及系统表(System Table)与句柄数据库(Handle Database)的组织方式展开,帮助读者建立从规范文本到实际固件代码的完整认知映射。
上图展示了UEFI基础架构中四个核心组件之间的逻辑关系。规范体系定义了所有接口与数据结构;系统表作为全局入口点,向驱动和应用程序暴露服务;句柄数据库则维护了系统中所有设备与协议实例的注册信息;驱动模型通过绑定协议驱动设备,并依赖句柄数据库完成设备管理。下面我们将逐一深入剖析。
2.1 UEFI规范体系:从规范文本到固件实现
核心原理:规范的分层结构与关键章节
UEFI规范(当前主要版本为2.10)由UEFI论坛维护,其内容涵盖固件、操作系统加载器(OS Loader)以及设备驱动之间的所有接口定义。规范文本并非线性叙述,而是按照功能域划分为多个章节,其中与固件开发者最密切相关的包括:
- 第2章(Protocols):定义了所有标准协议,如EFI_LOADED_IMAGE_PROTOCOL、EFI_SIMPLE_FILE_SYSTEM_PROTOCOL等,这是驱动与应用程序交互的基础。
- 第4章(Services):详细描述了启动时服务与运行时服务的函数原型、参数及返回值。
- 第7章(Devices):规定了设备路径(Device Path)的格式与编码方式,用于唯一标识系统中的设备。
- 第8章(Runtime Services):专门阐述变量服务、时间服务、复位服务等操作系统运行期间仍可调用的接口。
规范还定义了固件与操作系统之间的控制权交接流程,即著名的ExitBootServices()调用。该调用是启动时服务与运行时服务的分水岭:一旦调用成功,启动时服务占用的内存将被回收,固件进入“运行时”状态。
判断方法:如何快速定位规范中的关键信息
在实际开发中,工程师经常需要查阅规范。建议采用以下路径快速定位:
- 确认所需接口属于“协议”还是“服务”。协议通常以
EFI_XXX_PROTOCOL命名,服务则以EFI_BOOT_SERVICES或EFI_RUNTIME_SERVICES结构体中的函数指针形式出现。 - 在规范PDF中搜索协议GUID(Globally Unique Identifier)。每个协议都有唯一的128位GUID,例如
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL的GUID为0x0964e5b22-6459-11d2-8e39-00a0c969723b。 - 查阅协议结构体定义时,注意区分“成员变量”与“函数指针”。函数指针即为该协议提供的方法,成员变量则描述协议的状态或属性。
- 通过
gBS->AllocatePool()分配内存(类型为EfiBootServicesData),用于存放临时数据。 - 创建事件(
gBS->CreateEvent())用于异步通知,例如定时器事件或信号事件。 - 注册协议通知(
gBS->RegisterProtocolNotify())以监听特定协议的安装。 - 在驱动卸载或退出时,调用
gBS->FreePool()释放内存,并关闭所有打开的事件。 - Supported():驱动被要求检查某个句柄是否支持其设备。该函数不得修改硬件状态,仅做“探测”工作。
- Start():若Supported()返回成功,固件调用Start()让驱动接管设备。驱动在此函数中分配资源、安装子协议(如
EFI_BLOCK_IO_PROTOCOL)。 - Stop():当设备被移除或驱动需要卸载时,调用Stop()释放资源并关闭协议。
- 定义驱动入口点(
DriverEntry),安装EFI_DRIVER_BINDING_PROTOCOL和EFI_LOADED_IMAGE_PROTOCOL。 - 实现
Supported():检查设备路径或协议GUID,判断是否匹配。 - 实现
Start():打开设备句柄上的父协议(如EFI_USB_IO_PROTOCOL),创建子句柄并安装子协议。 - 实现
Stop():反向操作,关闭所有打开协议并释放内存。 FirmwareVendor:指向固件厂商名称字符串。FirmwareRevision:固件版本号。BootServices:指向EFI_BOOT_SERVICES结构体,内含所有启动时服务函数指针。RuntimeServices:指向EFI_RUNTIME_SERVICES结构体。NumberOfTableEntries与ConfigurationTable:指向系统配置表数组,如ACPI表、SMBIOS表等。- 调用
gBS->LocateHandleBuffer(),传入协议GUID,获取所有句柄的数组。 - 遍历数组,对每个句柄调用
gBS->HandleProtocol()获取协议接口。 - 使用完毕后,调用
gBS->FreePool()释放句柄数组。
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL用于文本输出,但具体是操作串口、VGA还是帧缓冲,由固件厂商自行决定。因此,阅读规范时不要试图寻找具体算法或硬件操作细节。
案例拆解:从规范到EDK2代码的映射
以EFI_LOADED_IMAGE_PROTOCOL为例,该协议描述了已加载的UEFI映像(如驱动或应用程序)的信息。规范中定义其结构体包含ImageBase、ImageSize、LoadOptions等字段。在EDK2(EFI Development Kit II)开源实现中,该协议在MdePkg/Include/Protocol/LoadedImage.h中定义,与规范完全一致。当固件加载一个EFI应用程序时,会创建一个句柄并安装该协议,应用程序通过HandleProtocol()或OpenProtocol()获取自身信息。这一映射关系是理解“规范-实现”对应性的最佳范例。
常见误区与总结复盘
总结:UEFI规范体系是固件开发的“宪法”,掌握其分层结构、GUID定位方法以及协议/服务的区分,是后续学习的基础。
2.2 启动时服务与运行时服务:生命周期与职责边界
核心原理:两类服务的本质区别
UEFI固件提供两类服务,它们的生命周期以ExitBootServices()为界:
| 维度 | 启动时服务(Boot Services) | 运行时服务(Runtime Services) |
|---|---|---|
| 生命周期 | 固件启动至OS加载器调用ExitBootServices()之前 | 从固件启动到OS关机(包括OS运行期间) |
| 内存占用 | 占用常规内存,ExitBootServices()后释放 | 占用保留内存,OS不得覆盖 |
| 典型功能 | 内存分配(AllocatePages)、事件创建、协议管理、句柄操作 | 变量读写(GetVariable/SetVariable)、时间操作、复位(ResetSystem) |
| 调用者 | UEFI驱动、UEFI应用程序、OS Loader | UEFI驱动、OS内核(通过EFI System Table指针) |
| 虚拟地址转换 | 不涉及 | 需支持SetVirtualAddressMap()进行地址重映射 |
启动时服务中,AllocatePages()与FreePages()是内存管理的核心。固件通过内存描述符(EFI_MEMORY_DESCRIPTOR)管理物理内存,OS Loader在调用ExitBootServices()前必须获取最终的内存映射(GetMemoryMap())。运行时服务中的变量服务则用于在重启后保留配置数据,例如BootOrder、Timeout等。
操作流程:正确使用启动时服务
在驱动开发中,使用启动时服务需遵循以下流程:
OpenProtocol()与CloseProtocol()必须成对使用。打开协议时需指定打开模式(如EFI_OPEN_PROTOCOL_BY_DRIVER或EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL),关闭时需匹配对应模式,否则会导致协议引用计数泄漏,引发系统不稳定。
案例拆解:变量服务在固件设置中的应用
考虑一个固件设置项“Boot Timeout”。用户在BIOS Setup界面修改超时值后,固件调用gRT->SetVariable()将该值写入NVRAM。变量名为"Timeout",GUID为EFI_GLOBAL_VARIABLE,属性为EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS。下次启动时,BDS(Boot Device Select)阶段通过gRT->GetVariable()读取该值,决定等待用户按键的时间。若OS希望修改该值(例如通过efibootmgr工具),同样调用SetVariable(),但需注意OS运行阶段只能访问标记了EFI_VARIABLE_RUNTIME_ACCESS的变量。
常见误区与总结复盘
gRT->SetVariable()写入非易失性变量,可能导致NVRAM空间耗尽(通常为64KB~128KB)。频繁写入还会磨损Flash芯片。务必遵循“先查询剩余空间(QueryVariableInfo()),再写入”的流程。
总结:启动时服务是“开发者的游乐场”,提供了丰富的资源管理能力;运行时服务是“OS的持久通道”,必须谨慎使用并考虑虚拟地址转换。
2.3 UEFI驱动模型:DriverBinding协议与设备管理
核心原理:驱动模型的分层与绑定机制
UEFI驱动模型的核心是EFI_DRIVER_BINDING_PROTOCOL。该协议包含三个函数指针:Supported()、Start()和Stop()。其工作流程如下:
驱动模型还定义了“驱动绑定支持”的概念:一个驱动可以绑定多个设备(例如一个USB主机控制器驱动绑定多个USB端口),一个设备也可以被多个驱动分层绑定(例如磁盘驱动绑定到USB Mass Storage驱动之上)。
判断方法:如何编写一个符合规范的驱动
编写UEFI驱动时,需遵循以下步骤:
Start()中打开父协议时,应使用EFI_OPEN_PROTOCOL_BY_DRIVER模式,并保存返回的协议接口指针。切勿在Supported()中打开协议,因为该函数可能被多次调用,且不应产生副作用。
案例拆解:NVMe驱动绑定流程
假设固件需要支持NVMe SSD。在DXE阶段,PCIe枚举器创建了NVMe控制器的句柄,并安装了EFI_PCI_IO_PROTOCOL。NVMe驱动(NvmExpressDxe)的Supported()检查该句柄的PCI配置空间,判断Class Code是否为0x010802(Mass Storage / Non-Volatile Memory controller)。若匹配,Start()则通过EFI_PCI_IO_PROTOCOL映射BAR寄存器,初始化控制器,并安装EFI_BLOCK_IO_PROTOCOL。随后,分区驱动(PartitionDxe)绑定该EFI_BLOCK_IO_PROTOCOL,创建子句柄并安装EFI_SIMPLE_FILE_SYSTEM_PROTOCOL。最终,OS Loader通过文件系统协议读取EFI\BOOT\BOOTX64.EFI。这一链条完美展示了驱动模型的分层绑定能力。
常见误区与总结复盘
Stop())时,必须确保所有子句柄上的协议已被移除,否则系统会留下“孤儿句柄”。EDK2提供了gBS->UninstallMultipleProtocolInterfaces()来原子性地卸载多个协议,推荐使用。
总结:UEFI驱动模型通过DriverBinding协议实现了设备与驱动的动态匹配,其分层设计使得复杂设备栈(如PCIe→NVMe→Partition→FileSystem)得以清晰构建。
2.4 系统表与句柄数据库:固件的“全局变量”与“设备注册中心”
核心原理:EFI_SYSTEM_TABLE与句柄数据库的结构
EFI_SYSTEM_TABLE是固件传递给UEFI应用程序和OS Loader的入口指针,其结构体包含以下关键字段:
句柄数据库是固件内部维护的一个链表结构,每个节点包含一个句柄(EFI_HANDLE)和一组协议接口。句柄本身是一个不透明的指针,用于标识设备、驱动或映像。协议接口则是指向协议结构体实例的指针。固件通过InstallProtocolInterface()和UninstallProtocolInterface()维护该数据库。
操作流程:遍历句柄数据库查找特定协议
在驱动开发中,经常需要查找系统中所有支持某协议的设备。标准流程如下:
EFI_STATUS Status;
EFI_HANDLE *HandleBuffer = NULL;
UINTN HandleCount = 0;
EFI_BLOCK_IO_PROTOCOL *BlockIo = NULL;
Status = gBS->LocateHandleBuffer(
ByProtocol,
&gEfiBlockIoProtocolGuid,
NULL,
&HandleCount,
&HandleBuffer
);
if (EFI_ERROR(Status)) return Status;
for (UINTN Index = 0; Index < HandleCount; Index++) {
Status = gBS->HandleProtocol(
HandleBuffer[Index],
&gEfiBlockIoProtocolGuid,
(VOID **)&BlockIo
);
if (!EFI_ERROR(Status)) {
// 使用BlockIo进行操作
}
}
gBS->FreePool(HandleBuffer);
案例拆解:通过系统表获取ACPI表
OS Loader或诊断工具常需获取ACPI RSDP(Root System Description Pointer)。通过EFI_SYSTEM_TABLE的ConfigurationTable,遍历每一项,比较其VendorGuid是否为ACPI_TABLE_GUID(0xeb9d2d30-2d88-11d3-9a16-0090273fc14d),即可获得RSDP地址。这一过程不依赖任何协议,仅通过系统表即可完成,体现了系统表作为“全局信息中心”的价值。
常见误区与总结复盘
ExitBootServices()后不再有效。OS内核不能直接使用gBS->LocateHandle()等启动时服务。OS必须通过ACPI、UEFI运行时服务或MMIO来访问硬件资源。因此,固件驱动若需将信息传递给OS,应通过配置表或运行时变量,而非句柄数据库。
总结:系统表是固件向OS开放的“窗口”,句柄数据库是固件内部的“设备注册表”。理解两者的关系,有助于设计出既符合规范又易于调试的固件模块。
本章从规范体系出发,逐步深入到服务、驱动模型和数据结构,为后续学习固件卷(FV)的封装格式与EFI启动文件系统(ESP)的布局奠定了坚实的架构基础。掌握这些核心概念,读者将能够理解固件映像如何在启动阶段被加载、驱动如何被绑定,以及OS如何通过标准接口与固件交互。