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 里都有一个句柄。系统通过句柄来管理所有对象。
句柄数据库的核心操作就四个:
- InstallProtocolInterface():给句柄安装协议。相当于“给这个人贴上标签”。
- ReinstallProtocolInterface():替换协议。驱动升级时常用。
- UninstallProtocolInterface():卸载协议。驱动停止时调用。
- 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()。
我画了一张图,帮你理解驱动栈的层次关系:
驱动栈的构建,核心是 依赖关系。上层驱动依赖下层驱动提供的协议。如果下层驱动没启动,上层驱动就找不到需要的协议,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 驱动栈的“优雅销毁”
驱动栈的销毁,比构建更讲究。为什么?因为销毁时,你要保证所有依赖关系都解开了。我总结了一个“三步走”策略:
- 通知上层驱动:调用上层驱动的
Stop(),让它们先卸载。 - 卸载自己的协议:调用
UninstallProtocolInterface(),把自己安装的协议从句柄上移除。 - 释放硬件资源:关闭 MMIO 映射、释放 DMA 缓冲区、重置硬件状态。
这里有个细节:UninstallProtocolInterface() 会检查还有没有其他驱动在引用这个协议。如果有引用计数不为零,卸载会失败。所以,销毁顺序必须是从上到下,不能乱来。
警告:千万不要在 Stop() 里调用 gBS->Exit() 或 gBS-> Stall()。前者会导致系统直接退出,后者会阻塞整个事件循环。我见过有人想在 Stop() 里加个延时等硬件复位,结果系统直接卡死。
好了,关于 UEFI 驱动模型的四个核心部分,我就讲到这里。驱动绑定协议是入口,I/O 抽象是桥梁,句柄数据库是基础设施,驱动栈是最终的组织形式。把这四块吃透了,你写 UEFI 驱动时就能少走很多弯路。