4、UEFI驱动模型:驱动绑定协议、I/O抽象与驱动栈构建
驱动模型,说白了就是UEFI固件里管理硬件设备的「规矩」。我刚接触UEFI时,总觉得这玩意儿绕来绕去,不如传统BIOS的int 13h来得直接。后来被项目毒打了几次,才明白这套模型的设计有多精妙。
这一章,我们重点聊三个东西:驱动绑定协议(Driver Binding Protocol)、I/O抽象,以及驱动栈是怎么搭起来的。你想想看,一个PCIe网卡插到主板上,UEFI怎么知道该加载哪个驱动?驱动怎么找到它管理的设备?多个驱动之间怎么协作?嗯,答案就在下面。
4.1 驱动绑定协议:驱动与设备的「红娘」
驱动绑定协议是UEFI驱动模型的核心。每个UEFI驱动都必须实现这个协议,否则系统根本不会理它。
这个协议就三个函数:Supported、Start、Stop。我习惯把它们叫做「三件套」。
| 函数 | 作用 | 调用时机 |
|---|---|---|
Supported |
检查驱动是否能管理某个设备 | 设备枚举时,系统遍历所有驱动 |
Start |
驱动接管设备,初始化硬件,安装I/O协议 | Supported返回成功之后 |
Stop |
驱动释放设备,卸载I/O协议 | 设备移除或驱动卸载时 |
流程其实很简单:系统枚举到一个新设备,就挨个问驱动「你能管它吗?」。驱动在Supported里检查设备ID、厂商ID、甚至特定寄存器。匹配上了,系统就调Start让驱动干活。
关键点:Supported不能修改硬件状态,只能「看一眼」。我曾经见过一个同事在Supported里初始化了硬件,结果设备被两个驱动同时认领,系统直接挂掉。嗯,血的教训。
4.1.1 驱动绑定协议的数据结构
来看看这个协议长什么样:
typedef struct _EFI_DRIVER_BINDING_PROTOCOL {
EFI_DRIVER_BINDING_PROTOCOL_SUPPORTED Supported;
EFI_DRIVER_BINDING_PROTOCOL_START Start;
EFI_DRIVER_BINDING_PROTOCOL_STOP Stop;
UINT32 Version;
EFI_HANDLE ImageHandle;
EFI_HANDLE DriverBindingHandle;
} EFI_DRIVER_BINDING_PROTOCOL;
Version字段用来区分驱动版本,系统会优先加载版本号高的驱动。ImageHandle是驱动自己的镜像句柄,DriverBindingHandle是驱动实例的句柄。这两个字段,说白了就是让驱动知道「我是谁」。
4.2 I/O抽象:让上层驱动「眼不见为净」
驱动接管设备后,不能直接暴露硬件寄存器给上层。为什么?因为上层驱动(比如文件系统驱动)根本不需要知道底层是SATA还是NVMe。它只需要一个读写接口。
这就是I/O抽象的作用。驱动在Start里安装一个协议,比如EFI_BLOCK_IO_PROTOCOL,把底层的读写操作封装起来。上层驱动只调用这个协议,不碰硬件。
我的习惯:写驱动时,先把I/O抽象协议定义清楚。协议里的函数接口越简单越好,参数越少越好。我见过一个同事把7个参数的函数塞进协议里,结果自己调试时都记不住参数顺序。你想想看,这得多痛苦。
4.2.1 典型的I/O抽象层次
从硬件到应用,I/O抽象大概分这么几层:
- 硬件层:PCI配置空间、MMIO寄存器、I/O端口
- 总线驱动层:PCI Bus Driver、USB Bus Driver,枚举设备,分配资源
- 设备驱动层:管理具体设备,安装I/O协议(如Block I/O、Serial I/O)
- 服务驱动层:文件系统、网络协议栈,调用设备驱动的I/O协议
每一层只跟相邻层打交道。这样做的好处是,换一块硬盘,只需要换设备驱动层,上面的文件系统驱动完全不用改。
4.3 驱动栈:层层叠叠的「千层饼」
驱动栈,就是把多个驱动按层次叠起来。每个驱动只负责一层,上层驱动调用下层驱动的协议。
举个例子,一个UEFI下的NVMe硬盘:
- PCI Bus Driver 枚举PCI总线,发现NVMe控制器
- NVMe Driver 初始化控制器,安装
EFI_BLOCK_IO_PROTOCOL - Partition Driver 读取分区表,为每个分区安装
EFI_BLOCK_IO_PROTOCOL(子协议) - FAT Driver 在分区上安装
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL
你看,一个读文件的操作,要穿过四层驱动。每一层都只做一件事,但合起来就能完成复杂功能。
注意:驱动栈不是越深越好。每多一层,就多一次函数调用开销,也多一层出错的可能。我曾经在一个项目里看到七层驱动栈,调试时查一个bug查了三天,最后发现是中间某层传参数传错了。嗯,从那以后我定了个规矩:驱动栈不超过四层。
4.3.1 驱动栈的构建过程
系统是怎么把驱动栈搭起来的?核心机制是设备句柄和协议安装。
- 总线驱动枚举设备,为每个设备创建一个
EFI_HANDLE,并安装设备路径协议 - 系统遍历所有驱动,调用
Supported检查匹配 - 匹配的驱动调用
Start,初始化设备,安装I/O协议 - 新安装的协议触发系统再次遍历驱动,寻找能管理这个协议的上层驱动
- 重复步骤2-4,直到没有新驱动可以绑定
这个过程是自动的。你只需要写好自己的驱动,系统会帮你搭栈。
4.4 核心逻辑:一张图看懂驱动模型
下面这张图,是我画了无数遍才定稿的。它把驱动模型的核心逻辑串起来了。
这张图里,红色节点是判断逻辑,绿色和紫色是两条分支。你注意看最下面的蓝色节点——安装协议后会触发新一轮遍历,这就是驱动栈能层层搭建的原因。
4.5 实战:写一个简单的驱动绑定协议
光说不练假把式。我们写一个最简单的驱动绑定协议实现。这个驱动管理一个虚拟设备,只做一件事:在Start里打印一行日志。
EFI_STATUS
EFIAPI
VirtualDeviceSupported (
IN EFI_DRIVER_BINDING_PROTOCOL *This,
IN EFI_HANDLE ControllerHandle,
IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath
)
{
// 检查设备路径,确认是不是我们的设备
// 这里简化处理:只检查设备路径是否存在
if (RemainingDevicePath == NULL) {
return EFI_UNSUPPORTED;
}
return EFI_SUCCESS;
}
EFI_STATUS
EFIAPI
VirtualDeviceStart (
IN EFI_DRIVER_BINDING_PROTOCOL *This,
IN EFI_HANDLE ControllerHandle,
IN EFI_DEVICE_PATH_PROTOCOL *RemainingDevicePath
)
{
EFI_STATUS Status;
// 初始化硬件(这里用打印代替)
DEBUG ((EFI_D_INFO, "VirtualDevice: Start called\n"));
// 安装 I/O 协议
Status = gBS->InstallMultipleProtocolInterfaces (
&ControllerHandle,
&gEfiVirtualDeviceProtocolGuid,
&mVirtualDeviceProtocol,
NULL
);
return Status;
}
EFI_STATUS
EFIAPI
VirtualDeviceStop (
IN EFI_DRIVER_BINDING_PROTOCOL *This,
IN EFI_HANDLE ControllerHandle,
IN UINTN NumberOfChildren,
IN EFI_HANDLE *ChildHandleBuffer
)
{
// 卸载协议,清理硬件
gBS->UninstallMultipleProtocolInterfaces (
ControllerHandle,
&gEfiVirtualDeviceProtocolGuid,
&mVirtualDeviceProtocol,
NULL
);
return EFI_SUCCESS;
}
// 驱动绑定协议实例
EFI_DRIVER_BINDING_PROTOCOL gVirtualDeviceDriverBinding = {
VirtualDeviceSupported,
VirtualDeviceStart,
VirtualDeviceStop,
0x10, // Version
NULL, // ImageHandle(由框架填充)
NULL // DriverBindingHandle(由框架填充)
};
避坑指南:我曾经在Start里忘了检查RemainingDevicePath,结果同一个设备被驱动绑定了两次。第二次Start时硬件已经初始化过了,直接导致系统崩溃。嗯,从那以后我每次都在Start开头加一句断言:ASSERT (RemainingDevicePath != NULL)。
4.6 驱动模型的几个关键设计原则
最后,我总结几条驱动模型的设计原则。这些原则不是我发明的,是UEFI规范里写的,但我在项目里吃过亏才真正理解它们。
- 一个驱动管理一类设备:不要写万能驱动。一个驱动只认一种设备ID,否则
Supported逻辑会乱成一锅粥。 - I/O协议要通用:协议接口不要跟具体硬件绑定。比如Block I/O协议,不管底层是SATA还是NVMe,上层看到的接口都一样。
- 驱动栈每层只做一件事:分区驱动只管分区,文件系统驱动只管文件。别把分区逻辑写到文件系统驱动里。
- 状态机要清晰:驱动要有明确的状态:未初始化、已初始化、已启动、已停止。每个状态下的行为要可预测。
嗯,驱动模型的内容就这些。你可能会觉得,不就是三个函数加一个协议吗?但就是这三个函数,撑起了整个UEFI的设备管理体系。写驱动时,多想想这三个函数该怎么实现,比什么都重要。