第四章 UEFI驱动模型与协议机制

UEFI固件与传统BIOS最本质的差异之一,在于其引入了面向对象的驱动模型与协议(Protocol)机制。在UEFI环境中,硬件设备的管理不再是通过硬编码的INT中断或I/O端口扫描,而是通过一套统一的、基于句柄(Handle)和协议(Protocol)的抽象层。本章将深入剖析UEFI驱动模型的核心架构,包括Driver Binding协议的工作方式、Start/Stop函数的生命周期管理、协议的安装与查找机制,以及句柄数据库(Handle Database)的维护原理。理解这些内容,是编写高质量UEFI驱动、Option ROM以及理解平台固件初始化流程的基石。

UEFI 驱动模型与协议机制核心架构 句柄数据库 (Handle Database) Handle A (PCIe NVMe) DevicePath | IoAttrib | DriverBinding Handle B (USB Keyboard) DevicePath | UsbIo | SimpleTextIn Handle C (Graphics) Gop | Edid | DevicePath 驱动绑定 (Driver Binding) NVM Express Driver Supported() → Start() USB Keyboard Driver Supported() → Start() Graphics Console Driver Supported() → Start() 协议接口 (Protocols) EFI_DRIVER_BINDING_PROTOCOL EFI_BLOCK_IO_PROTOCOL EFI_SIMPLE_TEXT_INPUT 匹配 匹配 安装 安装 驱动生命周期:Supported() → Start() → 运行 → Stop() 连接控制器 → 分配资源 → 安装协议 → 提供服务 → 断开时释放资源 ① 安装协议 InstallProtocolInterface ② 查找协议 LocateProtocol / HandleProtocol ③ 使用协议 调用协议接口函数 关键点:句柄是标识符,协议是接口,驱动是逻辑。三者通过句柄数据库关联,构成UEFI驱动模型的核心。

上图展示了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(用于指定设备路径中的特定子设备)。驱动必须通过以下步骤进行判断:

  1. 检查设备路径:使用EFI_DEVICE_PATH_PROTOCOL获取设备的路径,判断其类型(如PCI、USB、ACPI)和Vendor ID/Device ID是否匹配本驱动支持的硬件。
  2. 检查父协议:对于总线驱动,需要检查控制器上是否安装了父总线协议(如PCI Bus Protocol、USB Host Controller Protocol)。
  3. 尝试打开协议:使用OpenProtocol()以EFI_OPEN_PROTOCOL_BY_DRIVER属性打开父协议。如果打开失败,说明该协议已被其他驱动独占,则返回EFI_UNSUPPORTED。
  4. 注意: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供上层文件系统使用。

关键实现步骤:

  1. 驱动入口:在驱动入口点(DriverEntry)中,安装EFI_DRIVER_BINDING_PROTOCOL,并注册到系统。
  2. 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)。
  3. Start():分配私有数据结构,打开PCI IO协议(BY_DRIVER方式),配置PCI BAR(通常BAR0为NVMe寄存器),初始化NVMe控制器(等待就绪、设置队列),然后安装EFI_BLOCK_IO_PROTOCOL和EFI_DEVICE_PATH_PROTOCOL。
  4. 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),并严格遵循资源对称性原则,这样才能在复杂的固件环境中构建出高质量的驱动模块。