4. UEFI 协议模型:Protocol 的概念、安装与查找
好,我们进入第四章。这一章要聊的是 UEFI 里最核心的概念——Protocol。说白了,它就是 UEFI 世界的“接口标准”。你想想看,BIOS 里那么多模块,显卡驱动、硬盘驱动、文件系统……它们怎么互相认识?怎么调用对方的功能?靠的就是 Protocol。
我个人习惯把 Protocol 理解成“契约”。你实现了某个 Protocol,就等于你承诺“我能干这些事”。别人找到这个 Protocol,就知道怎么用你。这套机制让 UEFI 的驱动架构变得非常灵活。
4.1 Protocol 到底是什么?
从代码层面看,Protocol 就是一个结构体指针。这个结构体里放了一堆函数指针,还有可能带一些数据域。每个 Protocol 都有一个唯一的 GUID 来标识它。
举个例子,我们最常见的 EFI_LOADED_IMAGE_PROTOCOL:
typedef struct {
UINT32 Revision;
EFI_HANDLE ParentHandle;
EFI_SYSTEM_TABLE *SystemTable;
// ... 还有其他字段
EFI_IMAGE_ENTRY_POINT ImageEntryPoint;
} EFI_LOADED_IMAGE_PROTOCOL;
这个 Protocol 描述了当前加载的 UEFI 应用程序或驱动的信息。谁加载了它?它的入口点在哪?这些信息都封装在里面。
核心要点:Protocol = GUID + 结构体(含函数指针)。GUID 是“名字”,结构体是“内容”。
4.2 Protocol 的安装与查找
Protocol 不是凭空存在的。它需要被“安装”到一个 EFI_HANDLE 上。这个 Handle 你可以理解成一个“标签牌”,上面可以挂多个 Protocol。
安装 Protocol 最常用的函数是 gBS->InstallProtocolInterface()。不过在实际项目中,我更推荐用 gBS->InstallMultipleProtocolInterfaces(),因为它可以一次安装多个,还能自动处理错误回滚。
我遇到过一个问题:某个驱动在安装 Protocol 时,先装了 A,再装 B,结果 B 失败了。这时候 A 已经装上了,但驱动整体是失败的,导致系统里留下了一个“半残”的 Protocol。用 InstallMultipleProtocolInterfaces 就能避免这种尴尬。
查找 Protocol 呢?有两个主要函数:
gBS->HandleProtocol():给定一个 Handle 和 GUID,直接返回 Protocol 的指针。这是最常用的。gBS->LocateProtocol():如果你不知道 Handle 是谁,只想找到“第一个实现了这个 Protocol 的实例”,用这个。gBS->LocateHandleBuffer():找到所有实现了某个 Protocol 的 Handle 列表。
它们的区别我整理了一下:
| 函数名 | 输入 | 输出 | 典型场景 |
|---|---|---|---|
HandleProtocol |
Handle + GUID | Protocol 指针 | 已知设备 Handle,查它支持什么功能 |
LocateProtocol |
GUID | 第一个匹配的 Protocol 指针 | 全局只此一份的服务(如 GOP) |
LocateHandleBuffer |
GUID | Handle 数组 + 数量 | 枚举所有支持某协议的设备 |
我的小技巧:在调试阶段,我经常用 LocateHandleBuffer 来打印系统中所有实现了某个 Protocol 的设备数量。如果数量是 0,说明驱动没装上,或者 GUID 写错了。
4.3 Protocol 的接口与 GUID
GUID 是 128 位的唯一标识符。在 UEFI 里,它无处不在。每个 Protocol、每个 Variable、每个 Event 都有自己的 GUID。
GUID 的格式长这样:
#define EFI_LOADED_IMAGE_PROTOCOL_GUID \
{ 0x5B1B31A1, 0x9562, 0x11D2, \
{ 0x8E, 0x3F, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B } }
你可能会问:为什么不用字符串?用 GUID 多麻烦。嗯,这里有个历史原因。GUID 是全局唯一的,由算法生成,不会冲突。字符串“DiskIo”可能被不同厂商用同一个名字定义不同的结构体,但 GUID 不会。
我曾经踩过一个坑:两个驱动模块都定义了一个叫 MY_CUSTOM_PROTOCOL 的结构体,但内容不一样。结果一个模块安装,另一个模块查找,因为 GUID 不同,根本找不到。排查了半天才发现是 GUID 写错了。所以,GUID 一定要从规范里复制,不要手打。
4.4 Protocol 的依赖与通知
驱动之间是有依赖关系的。比如我的网卡驱动需要先有 PCI IO Protocol,才能去读写配置空间。UEFI 怎么处理这种依赖?
答案是 Depex(Dependency Expression)。每个 UEFI 驱动都可以在它的 EFI_DRIVER_BINDING_PROTOCOL 里声明自己依赖哪些 Protocol。UEFI 的 Dispatcher 会解析这个表达式,只有依赖满足时,才会调用驱动的 Supported() 和 Start()。
Depex 的语法有点像汇编:
// 伪代码示例
DEPEX {
EFI_PCI_IO_PROTOCOL_GUID
AND
EFI_DEVICE_PATH_PROTOCOL_GUID
}
意思是:这个驱动需要同时有 PCI IO 和 Device Path 两个 Protocol 才能启动。
除了 Depex,还有另一种机制叫 Protocol 通知。你可以注册一个回调函数,当某个 Protocol 被安装时,系统会通知你。这在热插拔场景下特别有用。
注册通知用 gBS->RegisterProtocolNotify():
EFI_EVENT Event;
gBS->RegisterProtocolNotify(
&gEfiSimpleFileSystemProtocolGuid, // 关注这个 Protocol
NewFileSystemArrived, // 回调函数
&Event
);
当有新的文件系统 Protocol 被安装时,NewFileSystemArrived 就会被调用。嗯,这里要注意:通知是异步的,回调里不能做太重的操作,否则会影响系统响应。
避坑指南:我曾经在 Protocol 通知回调里直接调用了 AllocatePages,结果导致系统死锁。因为通知回调是在 TPL_CALLBACK 级别执行的,而 AllocatePages 需要 TPL_NOTIFY 级别。记住:回调里只做标记,不要分配内存或操作硬件。
4.5 知识体系总览
这一章的内容比较多,我画了一张图帮你理清关系:
4.6 实战:写一个简单的 Protocol 消费者
光说不练假把式。我们写一段代码,演示如何查找并使用一个 Protocol。假设我们要找 EFI_SIMPLE_FILE_SYSTEM_PROTOCOL:
EFI_STATUS Status;
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *FileSystem;
EFI_HANDLE *HandleBuffer;
UINTN HandleCount;
UINTN Index;
// 方法一:直接找第一个实例
Status = gBS->LocateProtocol(
&gEfiSimpleFileSystemProtocolGuid,
NULL,
(VOID**)&FileSystem
);
if (EFI_ERROR(Status)) {
DEBUG((EFI_D_ERROR, "No file system protocol found!\n"));
return Status;
}
// 方法二:枚举所有支持该协议的设备
Status = gBS->LocateHandleBuffer(
ByProtocol,
&gEfiSimpleFileSystemProtocolGuid,
NULL,
&HandleCount,
&HandleBuffer
);
if (!EFI_ERROR(Status)) {
for (Index = 0; Index < HandleCount; Index++) {
DEBUG((EFI_D_INFO, "Device %d has file system\n", Index));
}
gBS->FreePool(HandleBuffer);
}
这段代码很简单,但很实用。我在做 UEFI Shell 扩展时,经常用这种方式来遍历系统中的文件系统设备。
补充一点:LocateHandleBuffer 返回的缓冲区需要用 gBS->FreePool 释放,否则会内存泄漏。这个坑我刚开始也踩过,后来养成了习惯:每次用完 HandleBuffer 立刻 Free。
好了,这一章的内容就到这里。Protocol 是 UEFI 的基石,理解了它,后面的驱动模型、Image 加载、Boot Manager 都会变得顺理成章。记住三个关键词:GUID、结构体、Handle。下次你看到 UEFI 代码里那些 gBS->LocateProtocol 的调用,应该就能会心一笑了。