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。
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 和你自定义的协议。
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 协议机制的整体流程,从定义到安装,再到发现和调用:
从这张图可以看得很清楚:协议机制本质上是一个发布-订阅模式。生产者发布协议到数据库,消费者从数据库订阅协议。双方通过 GUID 这个“暗号”对上号。
协议机制是 UEFI 的基石。后面讲驱动模型、安全启动签名验证时,你会发现所有功能都是建立在协议之上的。把这一章吃透,后面的路就好走了。