3. UEFI 驱动模型:驱动绑定协议、I/O 抽象、句柄数据库、驱动栈的构建与销毁

好,咱们今天聊点硬核的。UEFI 驱动模型,说白了就是一套让硬件和软件能“对上话”的规矩。你想想看,BIOS 要管理那么多设备——显卡、网卡、硬盘、USB 控制器——每个设备的初始化方式都不一样。如果没有一套统一的模型,那代码得乱成什么样?

我刚开始接触 UEFI 时,也觉得这东西挺绕。后来调了几个驱动,踩了几次坑,才慢慢摸清它的门道。今天我就把这套模型的四个核心部分拆开来讲:驱动绑定协议、I/O 抽象、句柄数据库,还有驱动栈的构建与销毁。

3.1 驱动绑定协议:驱动与设备的“红娘”

驱动绑定协议,就是 EFI_DRIVER_BINDING_PROTOCOL。它有三个关键函数:Supported()、Start() 和 Stop()。嗯,名字很直白,但背后的逻辑值得细品。

  • Supported():检查驱动能不能管这个设备。说白了就是“你行不行?”
  • Start():驱动开始干活,初始化设备,安装协议。
  • Stop():驱动停止,卸载协议,清理资源。

我个人习惯,在写驱动时,Supported() 里一定要做足检查。别光看设备 ID 匹配就返回成功,还得看看设备是不是已经被别的驱动占用了。我曾经遇到过一个问题:两个驱动都声称支持同一个设备,结果 Start() 被调了两次,设备直接挂了。

核心要点:驱动绑定协议是 UEFI 驱动模型的基石。没有它,系统根本不知道哪个驱动该管哪个设备。

3.2 I/O 抽象:让驱动和设备“说同一种语言”

设备千差万别,但 UEFI 希望驱动能用统一的方式访问它们。这就是 I/O 抽象的作用。常见的 I/O 抽象协议包括:

协议名称 用途 典型场景
EFI_PCI_IO_PROTOCOL 访问 PCI 配置空间、I/O 端口、MMIO 网卡、显卡、存储控制器
EFI_USB_IO_PROTOCOL USB 设备的控制/批量/中断传输 键盘、鼠标、U 盘
EFI_BLOCK_IO_PROTOCOL 块设备读写 硬盘、SSD、光驱
EFI_SIMPLE_TEXT_INPUT_PROTOCOL 键盘输入 控制台

你想想看,如果没有这些抽象协议,每个驱动都得自己写 PCI 配置空间访问代码,那得多乱?I/O 抽象把底层细节封装好了,驱动只需要调用协议接口就行。

避坑指南:我曾经在调试一个 NVMe 驱动时,发现读写总是超时。查了半天,原来是 EFI_PCI_IO_PROTOCOL 的 Mem.Read() 调用时,地址对齐没处理好。记住:UEFI 的 I/O 抽象对对齐要求很严格,尤其是 ARM 平台。

3.3 句柄数据库:系统的“设备通讯录”

句柄数据库,就是 gBS->HandleDatabase。每个设备、每个驱动、每个协议,在 UEFI 里都有一个句柄。系统通过句柄来管理所有对象。

句柄数据库的核心操作就四个:

  1. InstallProtocolInterface():给句柄安装协议。相当于“给这个人贴上标签”。
  2. ReinstallProtocolInterface():替换协议。驱动升级时常用。
  3. UninstallProtocolInterface():卸载协议。驱动停止时调用。
  4. HandleProtocol() / OpenProtocol():查询句柄上的协议。驱动找设备时用。

我记得有一次,一个同事写的驱动在 Stop() 里忘了调用 UninstallProtocolInterface(),结果句柄数据库里残留了一堆无效协议。系统启动到一半就崩了。嗯,这种问题最难查,因为错误不会立刻暴露。

警告:句柄数据库不是线程安全的。UEFI 虽然是单线程模型,但在某些平台(比如支持 SMP 的 ARM 服务器)上,多个核心同时访问句柄数据库会导致崩溃。我建议在操作句柄数据库时,加上 gBS->RaiseTPL() 保护。

3.4 驱动栈的构建与销毁

驱动栈,就是多个驱动按层次叠在一起。比如一个 NVMe 硬盘:

  • 最底层:PCI 总线驱动,枚举 PCI 设备,安装 EFI_PCI_IO_PROTOCOL。
  • 中间层:NVMe 控制器驱动,基于 EFI_PCI_IO_PROTOCOL 初始化 NVMe 控制器,安装 EFI_BLOCK_IO_PROTOCOL。
  • 最上层:分区驱动,基于 EFI_BLOCK_IO_PROTOCOL 识别分区,安装 EFI_SIMPLE_FILE_SYSTEM_PROTOCOL。

驱动栈的构建过程,就是每个驱动在 Start() 里安装新的协议,供上层驱动使用。销毁过程则相反:从顶层开始,逐层调用 Stop()。

我画了一张图,帮你理解驱动栈的层次关系:

UEFI 驱动栈层次结构 文件系统驱动 EFI_SIMPLE_FILE_SYSTEM_PROTOCOL 分区驱动 EFI_BLOCK_IO_PROTOCOL 设备驱动(如 NVMe 控制器) EFI_PCI_IO_PROTOCOL PCI 总线驱动 PCI 配置空间 / MMIO 驱动栈构建顺序:从下往上(PCI → 设备 → 分区 → 文件系统) 驱动栈销毁顺序:从上往下(文件系统 → 分区 → 设备 → PCI)

驱动栈的构建,核心是 依赖关系。上层驱动依赖下层驱动提供的协议。如果下层驱动没启动,上层驱动就找不到需要的协议,Supported() 会返回失败。

销毁时,顺序必须反过来。我见过有人先停掉 PCI 总线驱动,结果上层驱动还在用 EFI_PCI_IO_PROTOCOL,直接导致系统崩溃。嗯,这种问题在调试时特别难定位,因为崩溃现场往往离真正的原因很远。

3.5 实战中的驱动绑定流程

咱们来看一个典型的驱动绑定流程。假设你写了一个简单的 UEFI 驱动:

EFI_STATUS
EFIAPI
MyDriverSupported (
  IN EFI_DRIVER_BINDING_PROTOCOL  *This,
  IN EFI_HANDLE                   ControllerHandle,
  IN EFI_DEVICE_PATH_PROTOCOL     *RemainingDevicePath
  )
{
  EFI_STATUS  Status;
  EFI_PCI_IO_PROTOCOL  *PciIo;

  // 检查设备是否支持 PCI I/O 协议
  Status = gBS->OpenProtocol (
                ControllerHandle,
                &gEfiPciIoProtocolGuid,
                (VOID **)&PciIo,
                This->DriverBindingHandle,
                ControllerHandle,
                EFI_OPEN_PROTOCOL_BY_DRIVER
                );
  if (EFI_ERROR (Status)) {
    return Status;
  }

  // 检查设备 ID 是否匹配
  // ... 这里省略具体的 PCI 配置空间读取逻辑

  gBS->CloseProtocol (ControllerHandle, &gEfiPciIoProtocolGuid,
         This->DriverBindingHandle, ControllerHandle);

  return EFI_SUCCESS;
}

这段代码里,OpenProtocol() 用的是 EFI_OPEN_PROTOCOL_BY_DRIVER。这个标志很重要——它告诉系统:“这个协议我暂时借用一下,检查完就还回去。” 如果检查通过,Start() 里会再用 EFI_OPEN_PROTOCOL_BY_DRIVER 打开一次,这次就不关了,直到 Stop() 才释放。

个人经验:我建议在 Supported() 里尽量少做事。只做必要的检查,别初始化硬件。因为 Supported() 可能会被调用很多次——系统在枚举设备时,会对每个设备调用每个驱动的 Supported()。如果你在里面做耗时操作,启动速度会慢得让人抓狂。

3.6 句柄数据库的“暗坑”

句柄数据库看起来简单,但用起来有不少坑。我挑几个常见的说说:

  • 协议重复安装:同一个句柄上,不能安装两个相同 GUID 的协议。如果你不小心调了两次 InstallProtocolInterface(),第二次会失败。我建议在安装前先查一下。
  • 句柄泄漏:每次 OpenProtocol() 都要对应一次 CloseProtocol()。忘了关?句柄数据库里会留下一个“打开计数”,导致驱动无法卸载。
  • 协议版本不匹配:有些协议有版本号(比如 EFI_PCI_IO_PROTOCOL 有 Rev 2 和 Rev 3)。如果你的驱动只支持 Rev 3,但设备只提供 Rev 2,那 OpenProtocol() 会返回 EFI_UNSUPPORTED。

嗯,说到句柄泄漏,我想起一个真实案例。有个团队做 UEFI 网卡驱动,每次 PXE 启动后,驱动都卸载不干净。查了三天,发现是 Stop() 里少调了一个 CloseProtocol()。系统里残留了 200 多个打开的协议句柄,内存都快被吃光了。

3.7 驱动栈的“优雅销毁”

驱动栈的销毁,比构建更讲究。为什么?因为销毁时,你要保证所有依赖关系都解开了。我总结了一个“三步走”策略:

  1. 通知上层驱动:调用上层驱动的 Stop(),让它们先卸载。
  2. 卸载自己的协议:调用 UninstallProtocolInterface(),把自己安装的协议从句柄上移除。
  3. 释放硬件资源:关闭 MMIO 映射、释放 DMA 缓冲区、重置硬件状态。

这里有个细节:UninstallProtocolInterface() 会检查还有没有其他驱动在引用这个协议。如果有引用计数不为零,卸载会失败。所以,销毁顺序必须是从上到下,不能乱来。

警告:千万不要在 Stop() 里调用 gBS->Exit() 或 gBS-> Stall()。前者会导致系统直接退出,后者会阻塞整个事件循环。我见过有人想在 Stop() 里加个延时等硬件复位,结果系统直接卡死。

好了,关于 UEFI 驱动模型的四个核心部分,我就讲到这里。驱动绑定协议是入口,I/O 抽象是桥梁,句柄数据库是基础设施,驱动栈是最终的组织形式。把这四块吃透了,你写 UEFI 驱动时就能少走很多弯路。