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 知识体系总览

这一章的内容比较多,我画了一张图帮你理清关系:

UEFI Protocol 核心模型 Protocol GUID(唯一标识) 结构体(数据+函数) Handle(挂载点) 安装:InstallProtocolInterface 查找:LocateProtocol / HandleProtocol 通知:RegisterProtocolNotify 依赖机制:Depex(条件表达式) + 通知(异步回调) 核心思想:Protocol 是 UEFI 驱动模型的“粘合剂” 安装 → 查找 → 使用,构成了 UEFI 驱动的完整生命周期

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 的调用,应该就能会心一笑了。