第四章 UEFI驱动模型与协议机制
UEFI固件与传统BIOS最本质的差异之一,在于其引入了面向对象的驱动模型与协议(Protocol)机制。在UEFI环境中,硬件设备的管理不再是通过硬编码的INT中断或I/O端口扫描,而是通过一套统一的、基于句柄(Handle)和协议(Protocol)的抽象层。本章将深入剖析UEFI驱动模型的核心架构,包括Driver Binding协议的工作方式、Start/Stop函数的生命周期管理、协议的安装与查找机制,以及句柄数据库(Handle Database)的维护原理。理解这些内容,是编写高质量UEFI驱动、Option ROM以及理解平台固件初始化流程的基石。
上图展示了UEFI驱动模型的核心组件及其相互关系。句柄数据库作为系统全局的“注册表”,保存了所有设备句柄及其关联的协议实例。驱动绑定协议(Driver Binding Protocol)是驱动与设备之间的“媒人”,负责判断驱动是否支持某个设备(Supported),并在支持时启动驱动(Start)。协议接口则是驱动与上层应用通信的“契约”,定义了具体的数据结构和函数指针。下面我们将逐一深入这些机制。
4.1 句柄数据库:UEFI世界的“设备注册中心”
句柄数据库(Handle Database)是UEFI系统表(System Table)中维护的一个全局数据结构,它本质上是一个数组或链表,每个元素称为一个“句柄”(Handle)。句柄本身只是一个不透明的指针(void *),它本身不包含任何业务数据,其唯一的作用是作为标识符,用于关联一组协议接口。你可以将句柄想象成Windows设备管理器中的设备实例ID,而协议则是设备暴露出的功能接口(如读写、控制、状态查询)。
句柄数据库的核心操作由EFI_BOOT_SERVICES提供,主要包括以下函数:
- InstallProtocolInterface():在指定的句柄上安装一个协议接口。如果句柄为NULL,系统会创建一个新句柄。该操作是“创建设备”的底层实现。
- UninstallProtocolInterface():从句柄上移除一个协议接口。当最后一个协议被移除时,句柄本身也会被销毁。
- HandleProtocol():查询指定句柄上是否安装了特定的协议,如果安装了,则返回该协议的接口指针。这是最常用的查询函数。
- LocateHandle():根据协议GUID遍历句柄数据库,返回所有安装了该协议的句柄列表。常用于枚举设备。
- LocateHandleBuffer():LocateHandle的增强版,自动分配缓冲区并返回句柄数组。
- LocateProtocol():直接查找第一个安装了指定协议的句柄,并返回其协议接口。常用于获取系统级单例协议(如EFI_CPU_ARCH_PROTOCOL)。
核心原理:句柄数据库的设计遵循“组合优于继承”的原则。一个物理设备(如NVMe SSD)可能对应多个句柄,或者一个句柄上叠加多个协议。例如,一个NVMe控制器句柄上可能同时安装有EFI_DEVICE_PATH_PROTOCOL(描述设备位置)、EFI_BLOCK_IO_PROTOCOL(提供块读写能力)和EFI_NVM_EXPRESS_PASS_THRU_PROTOCOL(提供NVMe直通命令)。这种设计使得驱动可以按需“附加”功能,而不需要修改设备的核心数据结构。
4.2 驱动绑定协议:驱动的“匹配-启动-停止”生命周期
UEFI驱动模型的核心是EFI_DRIVER_BINDING_PROTOCOL。任何遵循UEFI驱动模型的驱动程序,都必须安装此协议。该协议包含三个关键函数指针:Supported()、Start()和Stop(),以及一个Version字段和ImageHandle、DriverBindingHandle。
| 函数 | 调用时机 | 职责 | 典型返回值 |
|---|---|---|---|
Supported() |
驱动被加载后,系统或总线枚举器尝试匹配设备时 | 检查控制器句柄是否支持该驱动。只做“探测”,不分配资源,不改变硬件状态。 | EFI_SUCCESS(支持)或 EFI_UNSUPPORTED(不支持) |
Start() |
Supported()返回成功后,由系统调用 | 正式启动驱动。分配必要的资源(内存、I/O、中断),初始化硬件,安装子协议(如Block IO),并可能创建子句柄。 | EFI_SUCCESS 或错误码(如EFI_OUT_OF_RESOURCES) |
Stop() |
设备被移除、系统进入退出启动服务(ExitBootServices)前,或驱动被卸载时 | 停止驱动。释放Start()中分配的所有资源,卸载子协议,恢复硬件到初始状态(如果可能)。 | EFI_SUCCESS 或 EFI_DEVICE_ERROR |
4.2.1 Supported() 函数的判断逻辑
Supported()是驱动与设备之间的“握手”。它接收ControllerHandle(待检测的设备句柄)和可选的RemainingDevicePath(用于指定设备路径中的特定子设备)。驱动必须通过以下步骤进行判断:
- 检查设备路径:使用
EFI_DEVICE_PATH_PROTOCOL获取设备的路径,判断其类型(如PCI、USB、ACPI)和Vendor ID/Device ID是否匹配本驱动支持的硬件。 - 检查父协议:对于总线驱动,需要检查控制器上是否安装了父总线协议(如PCI Bus Protocol、USB Host Controller Protocol)。
- 尝试打开协议:使用
OpenProtocol()以EFI_OPEN_PROTOCOL_BY_DRIVER属性打开父协议。如果打开失败,说明该协议已被其他驱动独占,则返回EFI_UNSUPPORTED。 - 注意:Supported()中不应修改硬件状态,也不应分配长期资源。如果探测需要临时读取硬件配置(如PCI配置空间),应在探测后恢复。
常见误区:初学者常犯的错误是在Supported()中执行了过多的硬件初始化操作。这会导致两个问题:一是如果多个驱动都尝试支持同一设备,系统会反复调用Supported(),造成不必要的硬件扰动;二是如果Supported()失败,硬件可能处于半初始化状态,导致后续驱动无法正常工作。正确的做法是:Supported()只做“只读”检查,真正的初始化放在Start()中。
4.2.2 Start() 与 Stop() 的资源对称性
Start()和Stop()必须严格对称。Start()中每分配一个资源(内存、事件、定时器、协议接口),Stop()中必须对应释放。UEFI规范强烈建议驱动遵循以下模式:
// Start() 典型流程
EFI_STATUS EFIAPI MyDriverStart(EFI_HANDLE ControllerHandle, EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath) {
EFI_STATUS Status;
MY_DEVICE *Device;
// 1. 分配设备私有数据结构
Device = AllocateZeroPool(sizeof(MY_DEVICE));
if (Device == NULL) return EFI_OUT_OF_RESOURCES;
// 2. 打开父协议(以BY_DRIVER方式)
Status = gBS->OpenProtocol(ControllerHandle, &gEfiPciIoProtocolGuid,
(VOID **)&Device->PciIo, gImageHandle,
ControllerHandle, EFI_OPEN_PROTOCOL_BY_DRIVER);
if (EFI_ERROR(Status)) {
FreePool(Device);
return Status;
}
// 3. 初始化硬件(配置PCI BAR、中断等)
Status = InitializeHardware(Device);
if (EFI_ERROR(Status)) {
gBS->CloseProtocol(ControllerHandle, &gEfiPciIoProtocolGuid, gImageHandle, ControllerHandle);
FreePool(Device);
return Status;
}
// 4. 安装子协议(如EFI_BLOCK_IO_PROTOCOL)
Device->BlockIo.Revision = EFI_BLOCK_IO_PROTOCOL_REVISION;
// ... 填充BlockIo函数指针 ...
Status = gBS->InstallProtocolInterface(&ControllerHandle, &gEfiBlockIoProtocolGuid,
EFI_NATIVE_INTERFACE, &Device->BlockIo);
if (EFI_ERROR(Status)) {
// 清理已分配资源
gBS->CloseProtocol(ControllerHandle, &gEfiPciIoProtocolGuid, gImageHandle, ControllerHandle);
FreePool(Device);
return Status;
}
// 5. 保存设备上下文到句柄的开放协议列表中
Device->Handle = ControllerHandle;
return EFI_SUCCESS;
}
// Stop() 典型流程(与Start严格对称)
EFI_STATUS EFIAPI MyDriverStop(EFI_HANDLE ControllerHandle, UINTN NumberOfChildren, EFI_HANDLE *ChildHandleBuffer) {
MY_DEVICE *Device;
EFI_STATUS Status;
// 1. 从控制器句柄上获取私有数据(通过协议接口反查)
Status = gBS->HandleProtocol(ControllerHandle, &gEfiBlockIoProtocolGuid, (VOID **)&Device);
if (EFI_ERROR(Status)) return EFI_DEVICE_ERROR;
// 2. 卸载子协议
gBS->UninstallProtocolInterface(ControllerHandle, &gEfiBlockIoProtocolGuid, &Device->BlockIo);
// 3. 关闭父协议
gBS->CloseProtocol(ControllerHandle, &gEfiPciIoProtocolGuid, gImageHandle, ControllerHandle);
// 4. 释放私有数据
FreePool(Device);
return EFI_SUCCESS;
}
实战经验:在编写Stop()时,务必考虑“部分失败”的情况。例如,如果Start()在第3步失败,Stop()不应该尝试释放第4步才安装的协议。建议在私有数据结构中增加一个“初始化状态”字段,记录当前已完成的步骤,Stop()根据该字段进行精确清理。
4.3 协议的安装、查找与使用
协议(Protocol)是UEFI中定义接口的标准方式。每个协议由一个GUID唯一标识,其结构体包含函数指针和数据字段。协议的安装与查找是驱动开发中最频繁的操作。
4.3.1 安装协议:InstallProtocolInterface 与 InstallMultipleProtocolInterfaces
安装协议时,驱动需要指定协议GUID、接口类型(通常为EFI_NATIVE_INTERFACE)和协议接口指针。推荐使用InstallMultipleProtocolInterfaces(),它允许一次原子性地安装多个协议,避免中间状态被其他驱动观察到。
// 推荐方式:一次安装多个协议
EFI_HANDLE Handle = NULL;
Status = gBS->InstallMultipleProtocolInterfaces(
&Handle,
&gEfiDevicePathProtocolGuid, DevicePath,
&gEfiBlockIoProtocolGuid, &BlockIo,
&gEfiSimpleFileSystemProtocolGuid, &SimpleFileSystem,
NULL // 必须以NULL结尾
);
4.3.2 查找协议:HandleProtocol 与 LocateProtocol
查找协议有两种典型场景:
- 已知句柄,查询协议:使用
HandleProtocol()。例如,在驱动绑定Supported()中,通过ControllerHandle查询EFI_PCI_IO_PROTOCOL。 - 未知句柄,全局搜索:使用
LocateProtocol()(获取第一个匹配的协议)或LocateHandleBuffer()(获取所有匹配的句柄)。例如,应用层需要获取EFI_SIMPLE_FILE_SYSTEM_PROTOCOL来访问文件系统。
关键区别:HandleProtocol()是“点查”,速度快,但要求调用者已经持有句柄。LocateProtocol()是“面查”,需要遍历句柄数据库,性能较差,但适合在初始化阶段获取系统级单例协议。在驱动运行期间,应尽量缓存已获取的协议指针,避免频繁调用LocateProtocol()。
4.3.3 协议打开方式与访问权限
UEFI引入了“协议打开”机制(OpenProtocol/CloseProtocol),用于管理协议的使用者。打开方式决定了访问权限和生命周期:
| 打开方式 | 属性值 | 含义 | 典型场景 |
|---|---|---|---|
| BY_DRIVER | 0x1 | 驱动独占该协议。如果协议已被其他驱动以BY_DRIVER方式打开,则OpenProtocol失败。 | 总线驱动打开父总线协议(如PCI IO) |
| BY_CHILD_CONTROLLER | 0x2 | 子控制器打开父控制器上的协议。用于建立父子设备关系。 | 磁盘驱动打开父控制器上的Block IO |
| BY_DRIVER | BY_CHILD_CONTROLLER | 0x3 | 驱动和子控制器共同打开。 | 复杂总线驱动 |
| BY_EXCLUSIVE | 0x10 | 独占访问,禁止任何其他方式打开。 | 关键系统资源(如CPU MP) |
| GET_PROTOCOL | 0x4 | 只获取协议指针,不建立依赖关系。不参与打开计数。 | 应用层临时查询 |
| TEST_PROTOCOL | 0x8 | 仅测试协议是否存在,不获取指针。 | Supported()中的快速检查 |
4.4 实战案例:编写一个简单的PCIe NVMe驱动绑定
下面通过一个简化的NVMe驱动绑定实现,串联本章所有概念。该驱动负责管理PCIe总线上的NVMe控制器。
场景设定:平台固件需要支持一块PCIe NVMe SSD。系统启动时,PCI Bus Driver枚举到该设备,创建了一个ControllerHandle,并安装了EFI_PCI_IO_PROTOCOL。我们的NVMe驱动需要绑定该句柄,并安装EFI_BLOCK_IO_PROTOCOL供上层文件系统使用。
关键实现步骤:
- 驱动入口:在驱动入口点(DriverEntry)中,安装
EFI_DRIVER_BINDING_PROTOCOL,并注册到系统。 - Supported():打开ControllerHandle上的
EFI_PCI_IO_PROTOCOL,读取PCI配置空间中的Vendor ID和Device ID,与NVMe设备的已知ID(如Vendor 0x144D, Device 0xA808)比对。同时检查设备类别码(Class Code)是否为Mass Storage Controller(0x01)且Subclass为NVM Express(0x08)。 - Start():分配私有数据结构,打开PCI IO协议(BY_DRIVER方式),配置PCI BAR(通常BAR0为NVMe寄存器),初始化NVMe控制器(等待就绪、设置队列),然后安装
EFI_BLOCK_IO_PROTOCOL和EFI_DEVICE_PATH_PROTOCOL。 - Stop():反向执行Start()的操作,卸载协议,关闭PCI IO,释放内存。
排障复盘:在调试过程中,我们遇到了一个典型问题:驱动加载后,Supported()返回成功,但Start()中初始化NVMe队列时失败(返回EFI_TIMEOUT)。通过添加调试打印,发现是BAR0的MMIO基址未正确映射。原因是PCI Bus Driver在枚举时,BAR的大小可能为0(如果设备未正确报告),导致分配失败。解决方案是在Start()中调用PciIo->Pci.Read()读取BAR寄存器,如果值为0,则使用PciIo->Pci.AllocateBuffer()手动分配MMIO空间,并调用PciIo->Pci.SetBarAttributes()设置地址。这个案例说明,驱动不能假设总线枚举器已经完美配置了所有资源,必须做好防御性编程。
风险提示:在Stop()中,如果设备正在被上层应用使用(例如文件系统正在读写),直接卸载协议会导致系统崩溃。正确的做法是:在Stop()中检查是否有子控制器(ChildHandleBuffer)存在,如果有,先递归停止子控制器,或者返回EFI_ACCESS_DENIED拒绝停止。UEFI规范要求驱动支持“延迟停止”机制,即当有子设备时,先停止子设备,再停止自身。
4.5 常见误区与设计总结
在UEFI驱动开发中,以下误区需要特别警惕:
- 误区一:在Supported()中修改硬件状态。这会导致驱动探测失败或系统不稳定。Supported()应保持“只读”语义。
- 误区二:忽略协议打开方式。如果两个驱动都以BY_DRIVER方式打开同一个协议,第二个驱动会失败。应使用
EFI_OPEN_PROTOCOL_BY_DRIVER时检查返回值,并考虑使用BY_CHILD_CONTROLLER来共享父协议。 - 误区三:句柄使用后不释放。在UEFI中,句柄是全局资源。如果驱动卸载时不调用
UninstallProtocolInterface(),句柄会泄漏,导致系统资源耗尽。 - 误区四:混淆HandleProtocol与OpenProtocol。HandleProtocol只是“查看”协议,不建立依赖关系;OpenProtocol会记录打开者,影响驱动的停止逻辑。在驱动中应使用OpenProtocol,在应用层可使用HandleProtocol。
- 误区五:Start()和Stop()不对称。这是最常见的Bug来源。建议在代码审查时,逐行对比Start()和Stop()中的资源分配与释放。
设计建议:对于复杂的设备驱动,建议采用“分层驱动”架构。例如,NVMe驱动分为传输层(管理PCIe和NVMe队列)和块IO层(将NVMe命令转换为Block IO接口)。每层使用独立的协议进行通信,这样既便于测试,也便于复用。同时,利用UEFI的EFI_DRIVER_SUPPORTED_EFI_VERSION和EFI_DRIVER_FAMILY字段,可以控制驱动的兼容性和加载优先级。
总结而言,UEFI驱动模型通过句柄数据库、驱动绑定协议和协议接口三者协同,实现了设备驱动的“即插即用”和“动态绑定”。掌握Supported/Start/Stop的生命周期管理,理解协议打开方式的语义,是编写健壮、可维护的UEFI驱动的基础。在实际开发中,建议充分利用UEFI规范提供的调试手段(如DEBUG()宏、EFI_LOADED_IMAGE_PROTOCOL),并严格遵循资源对称性原则,这样才能在复杂的固件环境中构建出高质量的驱动模块。