UEFI基础架构:UEFI规范体系、运行时与启动时服务、驱动模型、系统表与句柄数据库

UEFI(Unified Extensible Firmware Interface)并非一个单一的程序,而是一整套定义了操作系统与平台固件之间接口的规范体系。理解UEFI的基础架构,是深入掌握固件卷(FV)与EFI启动文件系统(ESP)的基石。本章将围绕UEFI规范体系的分层结构、启动时(Boot Time)与运行时(Runtime)服务的本质区别、驱动模型(Driver Model)的工作机制,以及系统表(System Table)与句柄数据库(Handle Database)的组织方式展开,帮助读者建立从规范文本到实际固件代码的完整认知映射。

UEFI基础架构核心组件关系图 UEFI规范体系(UEFI 2.x Spec) EFI系统表(EFI_SYSTEM_TABLE) 运行时服务表 + 启动时服务表 + 配置表 句柄数据库(Handle Database) 句柄 + 协议接口(Protocol Interface) 启动时服务(Boot Services) 内存管理 / 事件 / 协议接口 ExitBootServices() 后失效 运行时服务(Runtime Services) 变量 / 时间 / 复位 / 虚拟内存 OS接管后持续有效 UEFI驱动模型 DriverBinding协议 Start() / Stop() 驱动模型与句柄数据库的交互流程 1. 安装协议 2. 驱动绑定 3. 句柄创建 4. 协议消费 系统表/句柄 启动时服务 运行时服务 驱动模型

上图展示了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()调用。该调用是启动时服务与运行时服务的分水岭:一旦调用成功,启动时服务占用的内存将被回收,固件进入“运行时”状态。

判断方法:如何快速定位规范中的关键信息

在实际开发中,工程师经常需要查阅规范。建议采用以下路径快速定位:

  1. 确认所需接口属于“协议”还是“服务”。协议通常以EFI_XXX_PROTOCOL命名,服务则以EFI_BOOT_SERVICES或EFI_RUNTIME_SERVICES结构体中的函数指针形式出现。
  2. 在规范PDF中搜索协议GUID(Globally Unique Identifier)。每个协议都有唯一的128位GUID,例如EFI_SIMPLE_FILE_SYSTEM_PROTOCOL的GUID为0x0964e5b22-6459-11d2-8e39-00a0c969723b。
  3. 查阅协议结构体定义时,注意区分“成员变量”与“函数指针”。函数指针即为该协议提供的方法,成员变量则描述协议的状态或属性。
  4. 经验提醒:UEFI规范是“契约”而非“实现指南”。它只规定“做什么”和“接口长什么样”,不规定“怎么做”。例如,规范要求固件提供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规范与PI规范(Platform Initialization Specification)混淆。UEFI规范关注操作系统可见的接口,而PI规范关注固件内部初始化流程(如SEC、PEI、DXE、BDS阶段)。两者虽然同属UEFI论坛,但适用范围不同。开发固件驱动时主要参考UEFI规范;开发固件核心(如PEI模块)时则需参考PI规范。

    总结: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等。

    操作流程:正确使用启动时服务

    在驱动开发中,使用启动时服务需遵循以下流程:

    1. 通过gBS->AllocatePool()分配内存(类型为EfiBootServicesData),用于存放临时数据。
    2. 创建事件(gBS->CreateEvent())用于异步通知,例如定时器事件或信号事件。
    3. 注册协议通知(gBS->RegisterProtocolNotify())以监听特定协议的安装。
    4. 在驱动卸载或退出时,调用gBS->FreePool()释放内存,并关闭所有打开的事件。
    关键点:启动时服务中的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()。其工作流程如下:

    • Supported():驱动被要求检查某个句柄是否支持其设备。该函数不得修改硬件状态,仅做“探测”工作。
    • Start():若Supported()返回成功,固件调用Start()让驱动接管设备。驱动在此函数中分配资源、安装子协议(如EFI_BLOCK_IO_PROTOCOL)。
    • Stop():当设备被移除或驱动需要卸载时,调用Stop()释放资源并关闭协议。

    驱动模型还定义了“驱动绑定支持”的概念:一个驱动可以绑定多个设备(例如一个USB主机控制器驱动绑定多个USB端口),一个设备也可以被多个驱动分层绑定(例如磁盘驱动绑定到USB Mass Storage驱动之上)。

    判断方法:如何编写一个符合规范的驱动

    编写UEFI驱动时,需遵循以下步骤:

    1. 定义驱动入口点(DriverEntry),安装EFI_DRIVER_BINDING_PROTOCOL和EFI_LOADED_IMAGE_PROTOCOL。
    2. 实现Supported():检查设备路径或协议GUID,判断是否匹配。
    3. 实现Start():打开设备句柄上的父协议(如EFI_USB_IO_PROTOCOL),创建子句柄并安装子协议。
    4. 实现Stop():反向操作,关闭所有打开协议并释放内存。
    经验提醒:在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的入口指针,其结构体包含以下关键字段:

    • FirmwareVendor:指向固件厂商名称字符串。
    • FirmwareRevision:固件版本号。
    • BootServices:指向EFI_BOOT_SERVICES结构体,内含所有启动时服务函数指针。
    • RuntimeServices:指向EFI_RUNTIME_SERVICES结构体。
    • NumberOfTableEntries与ConfigurationTable:指向系统配置表数组,如ACPI表、SMBIOS表等。

    句柄数据库是固件内部维护的一个链表结构,每个节点包含一个句柄(EFI_HANDLE)和一组协议接口。句柄本身是一个不透明的指针,用于标识设备、驱动或映像。协议接口则是指向协议结构体实例的指针。固件通过InstallProtocolInterface()和UninstallProtocolInterface()维护该数据库。

    操作流程:遍历句柄数据库查找特定协议

    在驱动开发中,经常需要查找系统中所有支持某协议的设备。标准流程如下:

    1. 调用gBS->LocateHandleBuffer(),传入协议GUID,获取所有句柄的数组。
    2. 遍历数组,对每个句柄调用gBS->HandleProtocol()获取协议接口。
    3. 使用完毕后,调用gBS->FreePool()释放句柄数组。
    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如何通过标准接口与固件交互。