4. UEFI协议机制:协议的定义与安装、协议发现与定位、协议接口调用、自定义协议实战

UEFI 协议,说白了就是固件世界里的一套接口契约。我刚开始接触 UEFI 时,总觉得这东西有点绕——不就是个函数指针表吗?后来做多了才发现,这套机制是整个 UEFI 驱动模型的灵魂。没有协议,驱动之间就是孤岛,谁也找不到谁。

这一章,咱们就把协议从定义到实战彻底捋一遍。我会结合自己踩过的坑,把协议机制讲透。

4.1 协议的本质:接口与GUID

每个 UEFI 协议都由两部分组成:GUID 和 接口结构体。

  • GUID:128位的唯一标识符,用来区分不同的协议。就像每个人的身份证号。
  • 接口结构体:包含函数指针和数据字段的 C 结构体。驱动通过这个结构体对外提供服务。

举个例子,最简单的协议定义长这样:

// 协议 GUID
#define MY_SIMPLE_PROTOCOL_GUID \
  { 0x12345678, 0x9abc, 0xdef0, \
    { 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0 } }

// 协议接口结构体
typedef struct _MY_SIMPLE_PROTOCOL {
  UINT64    Revision;
  EFI_STATUS (*GetValue)(IN struct _MY_SIMPLE_PROTOCOL *This, OUT UINT32 *Value);
  EFI_STATUS (*SetValue)(IN struct _MY_SIMPLE_PROTOCOL *This, IN UINT32 Value);
} MY_SIMPLE_PROTOCOL;

注意看,结构体里的第一个成员通常是 Revision,用来标识协议版本。后面就是函数指针。每个函数的第一个参数都是 This 指针——这是 UEFI 的惯例,类似 C++ 的 this。

我的习惯:定义协议时,Revision 字段一定要用。我见过有人偷懒不写版本号,结果后面升级协议时,老的驱动和新驱动互相不兼容,查了半天才发现是版本没对齐。

4.2 协议的安装:把服务注册到系统

协议定义好了,怎么让其他模块能用?答案是安装到系统的协议数据库里。UEFI 提供了一组 Boot Service 来管理协议。

最常用的安装函数是 gBS->InstallProtocolInterface() 和 gBS->InstallMultipleProtocolInterfaces()。我个人更推荐后者,因为它可以一次安装多个协议,减少调用次数。

看个实际例子:

EFI_STATUS
InstallMyProtocol (
  IN EFI_HANDLE  Handle
  )
{
  MY_SIMPLE_PROTOCOL  *MyProtocol;

  // 分配协议实例内存
  MyProtocol = AllocateZeroPool (sizeof (MY_SIMPLE_PROTOCOL));
  if (MyProtocol == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  // 填充协议接口
  MyProtocol->Revision = 0x00010000;
  MyProtocol->GetValue = MyGetValue;
  MyProtocol->SetValue = MySetValue;

  // 安装到指定 Handle
  return gBS->InstallMultipleProtocolInterfaces (
                &Handle,
                &gMySimpleProtocolGuid,
                MyProtocol,
                NULL
                );
}

这里有个关键点:Handle 是什么?你可以把它理解成一个“挂衣钩”。协议实例挂在 Handle 上,一个 Handle 可以挂多个协议。比如一个 PCI 设备 Handle 上,可能同时挂着 gEfiPciIoProtocolGuid、gEfiDevicePathProtocolGuid 和你自定义的协议。

我曾经踩过的坑:安装协议时,Handle 参数传 NULL 会怎样?系统会帮你创建一个新 Handle。但如果你想让协议挂在一个已有的设备 Handle 上,必须传那个 Handle 的指针。我早期写代码时没注意,结果协议装到了新 Handle 上,其他驱动通过设备路径根本找不到它。

4.3 协议的发现与定位:怎么找到你要的协议

协议装好了,其他驱动怎么找到它?UEFI 提供了三种主要方式:

函数 用途 适用场景
gBS->HandleProtocol() 从指定 Handle 上获取协议接口 已知 Handle,查协议
gBS->LocateProtocol() 全局搜索第一个支持该协议的 Handle 不关心哪个 Handle,只要协议能用
gBS->LocateHandleBuffer() 找到所有支持该协议的 Handle 需要遍历所有提供该协议的设备

实际开发中,LocateProtocol() 用得最多。比如你想访问系统的 GOP(图形输出协议),直接搜就行:

EFI_GRAPHICS_OUTPUT_PROTOCOL  *Gop;
EFI_STATUS                    Status;

Status = gBS->LocateProtocol (
                &gEfiGraphicsOutputProtocolGuid,
                NULL,
                (VOID **)&Gop
                );
if (EFI_ERROR (Status)) {
  // 没有 GOP 协议,可能是纯文本模式
  return Status;
}

// 现在可以直接调用 GOP 接口
Gop->SetMode (Gop, 0);

你想想看,这种设计的好处是什么?驱动之间完全解耦。生产者只管安装协议,消费者只管搜索协议,双方不需要知道对方是谁。

4.4 协议接口调用:用起来很简单

找到协议指针后,调用接口就是标准的 C 函数调用。但有几个细节要注意:

  • 第一个参数永远是 This:指向协议实例本身。这样函数内部可以访问协议的数据字段。
  • 返回值永远是 EFI_STATUS:UEFI 规范要求所有协议函数返回状态码。调用后必须检查。
  • 调用前后可能需要处理状态:比如调用 GOP 的 Blt() 函数前,可能需要先获取当前模式信息。

举个例子,调用我们之前定义的自定义协议:

MY_SIMPLE_PROTOCOL  *MyProto;
UINT32              Value;
EFI_STATUS          Status;

// 先找到协议
Status = gBS->LocateProtocol (&gMySimpleProtocolGuid, NULL, (VOID **)&MyProto);
if (EFI_ERROR (Status)) {
  DEBUG ((EFI_D_ERROR, "MyProtocol not found!\n"));
  return Status;
}

// 调用 GetValue
Status = MyProto->GetValue (MyProto, &Value);
if (!EFI_ERROR (Status)) {
  DEBUG ((EFI_D_INFO, "Got value: %d\n", Value));
}

// 调用 SetValue
Status = MyProto->SetValue (MyProto, 0x42);

嗯,这里要注意:千万不要缓存协议指针。协议可能在运行时被卸载或替换。每次使用前重新 Locate 一次,虽然多了一次查找开销,但保证了安全性。我在一个项目中就是因为缓存了协议指针,结果驱动卸载后指针变成野指针,系统直接挂死。

4.5 自定义协议实战:从零写一个完整例子

光说不练假把式。咱们来写一个完整的自定义协议——一个简单的计算器协议。它提供加、减、乘、除四个接口。

第一步:定义协议头文件

#ifndef __CALCULATOR_PROTOCOL_H__
#define __CALCULATOR_PROTOCOL_H__

#define CALCULATOR_PROTOCOL_GUID \
  { 0xa1b2c3d4, 0xe5f6, 0x7890, \
    { 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89 } }

typedef struct _CALCULATOR_PROTOCOL CALCULATOR_PROTOCOL;

typedef
EFI_STATUS
(EFIAPI *CALCULATOR_ADD)(
  IN  CALCULATOR_PROTOCOL  *This,
  IN  INT64                A,
  IN  INT64                B,
  OUT INT64                *Result
  );

// ... 类似定义 Sub, Mul, Div

struct _CALCULATOR_PROTOCOL {
  UINT64          Revision;
  CALCULATOR_ADD  Add;
  CALCULATOR_SUB  Sub;
  CALCULATOR_MUL  Mul;
  CALCULATOR_DIV  Div;
};

extern EFI_GUID gCalculatorProtocolGuid;

#endif

第二步:实现生产者驱动

// 实现 Add 函数
EFI_STATUS
EFIAPI
CalculatorAdd (
  IN  CALCULATOR_PROTOCOL  *This,
  IN  INT64                A,
  IN  INT64                B,
  OUT INT64                *Result
  )
{
  *Result = A + B;
  return EFI_SUCCESS;
}

// 类似实现其他函数...

// 入口点:安装协议
EFI_STATUS
EFIAPI
CalculatorDriverEntryPoint (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS           Status;
  CALCULATOR_PROTOCOL  *Calculator;

  Calculator = AllocateZeroPool (sizeof (CALCULATOR_PROTOCOL));
  if (Calculator == NULL) {
    return EFI_OUT_OF_RESOURCES;
  }

  Calculator->Revision = 0x00010000;
  Calculator->Add      = CalculatorAdd;
  Calculator->Sub      = CalculatorSub;
  Calculator->Mul      = CalculatorMul;
  Calculator->Div      = CalculatorDiv;

  Status = gBS->InstallMultipleProtocolInterfaces (
                  &ImageHandle,
                  &gCalculatorProtocolGuid,
                  Calculator,
                  NULL
                  );
  return Status;
}

第三步:消费者驱动调用

EFI_STATUS
UseCalculator (
  VOID
  )
{
  CALCULATOR_PROTOCOL  *Calc;
  INT64                Result;
  EFI_STATUS           Status;

  Status = gBS->LocateProtocol (&gCalculatorProtocolGuid, NULL, (VOID **)&Calc);
  if (EFI_ERROR (Status)) {
    DEBUG ((EFI_D_ERROR, "Calculator protocol not found!\n"));
    return Status;
  }

  Calc->Add (Calc, 10, 20, &Result);   // Result = 30
  Calc->Sub (Calc, 100, 50, &Result);  // Result = 50
  Calc->Mul (Calc, 6, 7, &Result);     // Result = 42
  Calc->Div (Calc, 100, 3, &Result);   // Result = 33

  return EFI_SUCCESS;
}
实战要点总结:
  • 协议 GUID 必须全局唯一。可以用 uuidgen 工具生成。
  • 协议结构体中的 Revision 字段建议从 0x00010000 开始,方便后续兼容。
  • 所有协议函数必须遵循 UEFI 调用约定(EFIAPI)。
  • 生产者负责分配和释放协议实例内存,消费者只管调用。

4.6 协议机制全景图

下面这张图展示了 UEFI 协议机制的整体流程,从定义到安装,再到发现和调用:

UEFI协议机制全景图 UEFI协议机制全景图 阶段一:协议定义 GUID + 接口结构体 函数指针 + 数据字段 阶段二:协议安装 InstallMultipleProtocolInterfaces 协议 → Handle 协议数据库 Handle → 协议列表 全局注册表 阶段四:协议发现 LocateProtocol / HandleProtocol 按GUID搜索 阶段五:协议调用 This->Function(This, ...) 检查EFI_STATUS 关键设计原则 • 生产者/消费者解耦 • 通过GUID唯一标识 • 不缓存协议指针

从这张图可以看得很清楚:协议机制本质上是一个发布-订阅模式。生产者发布协议到数据库,消费者从数据库订阅协议。双方通过 GUID 这个“暗号”对上号。

我的建议:刚开始写 UEFI 驱动时,先从一个简单的自定义协议入手。比如我上面写的计算器协议,虽然功能简单,但把定义、安装、发现、调用的完整流程都走了一遍。等你把这个跑通了,再去理解更复杂的协议(比如 PCI IO、Block IO)就会轻松很多。

协议机制是 UEFI 的基石。后面讲驱动模型、安全启动签名验证时,你会发现所有功能都是建立在协议之上的。把这一章吃透,后面的路就好走了。