UEFI协议与平台服务开发工程 - 第4节
4.1 协议的核心概念
在 UEFI 固件体系中,协议(Protocol)是驱动与应用程序之间进行交互的核心抽象。一个协议本质上是一个由函数指针(接口方法)和数据结构组成的 接口契约。驱动通过安装协议来对外提供能力,应用程序或其它驱动则通过定位协议来使用这些能力。
协议的设计遵循面向对象思想:每个协议都有一个唯一的 GUID 作为标识,协议接口通常以 EFI_XXX_PROTOCOL 结构体形式定义,内部包含一组 EFIAPI 回调函数。
4.2 GUID 与协议标识
每个 UEFI 协议必须关联一个 全局唯一标识符(GUID)。GUID 是一个 128 位(16 字节)的值,格式为 XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX。系统通过 GUID 来区分、查找和匹配协议,而不是通过名称字符串。
| 概念 | 说明 | 示例 |
|---|---|---|
| GUID 定义 | 使用 #define 宏或 EFI_GUID 结构体声明 |
{ 0x09576e91, 0x6d3f, 0x11d2, ... } |
| 协议 GUID | 标识某个特定协议接口 | gEfiSimpleTextOutProtocolGuid |
| 用途 | 安装、定位、打开/关闭协议的唯一钥匙 | 所有协议操作 API 均以 GUID 为参数 |
在 EDK II 框架中,协议 GUID 通常在 .dec 文件中声明,并在 .h 头文件中通过 extern EFI_GUID 暴露给模块使用。
// 协议 GUID 声明示例(来自 MdePkg)
#define EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL_GUID \
{ 0x387477c2, 0x69c7, 0x11d2, \
{ 0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b } }
extern EFI_GUID gEfiSimpleTextOutProtocolGuid;
4.3 协议的安装:InstallProtocolInterface
驱动在初始化阶段,通过 InstallProtocolInterface 服务将协议安装到某个 设备句柄(Handle) 上。该服务由 UEFI 启动服务 gBS->InstallProtocolInterface() 提供。
函数原型
EFI_STATUS
EFIAPI
InstallProtocolInterface (
IN OUT EFI_HANDLE *Handle,
IN EFI_GUID *Protocol,
IN EFI_INTERFACE_TYPE InterfaceType,
IN VOID *Interface
);
参数说明
| 参数 | 含义 |
|---|---|
Handle |
指向句柄的指针。若为 NULL,系统会新建句柄;若指向已有句柄,则在该句柄上附加协议 |
Protocol |
要安装的协议 GUID 指针 |
InterfaceType |
接口类型,通常为 EFI_NATIVE_INTERFACE |
Interface |
协议接口结构体实例的指针,即协议函数表 |
典型安装流程
- 驱动入口函数中分配或静态定义一个协议接口实例。
- 填充协议结构体中的函数指针,指向驱动内部实现。
- 调用
gBS->InstallProtocolInterface(),将协议绑定到句柄。 - 系统将协议加入内部数据库,后续可通过 GUID 定位。
// 安装协议示例
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL mConOut;
EFI_HANDLE mHandle = NULL;
// 填充协议接口
mConOut.OutputString = MyOutputStringImpl;
mConOut.ClearScreen = MyClearScreenImpl;
// 安装到新句柄
Status = gBS->InstallProtocolInterface (
&mHandle,
&gEfiSimpleTextOutProtocolGuid,
EFI_NATIVE_INTERFACE,
&mConOut
);
4.4 协议的定位:LocateProtocol
消费者需要获取某个协议时,使用 LocateProtocol 服务。该服务在系统协议数据库中搜索第一个匹配指定 GUID 的协议实例,并返回其接口指针。
函数原型
EFI_STATUS
EFIAPI
LocateProtocol (
IN EFI_GUID *Protocol,
IN VOID *Registration OPTIONAL,
OUT VOID **Interface
);
参数说明
| 参数 | 含义 |
|---|---|
Protocol |
要查找的协议 GUID |
Registration |
注册通知的可选参数,通常传 NULL |
Interface |
输出参数,返回协议接口指针 |
// 定位协议示例
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut;
Status = gBS->LocateProtocol (
&gEfiSimpleTextOutProtocolGuid,
NULL,
(VOID **)&ConOut
);
if (!EFI_ERROR (Status)) {
ConOut->OutputString (ConOut, L"Hello UEFI!\r\n");
}
LocateProtocol 只返回第一个匹配的协议实例。如果需要枚举所有同类型协议或按条件筛选,应使用 LocateHandleBuffer + OpenProtocol 组合。
4.5 协议的打开与关闭:OpenProtocol / CloseProtocol
在 UEFI 2.0 及更高版本中,引入了更完善的协议访问管理机制。OpenProtocol 和 CloseProtocol 提供了对协议使用权的精细控制,支持 属性(Attributes) 来区分访问类型,并支持 通知(Notify) 机制。
OpenProtocol 函数原型
EFI_STATUS
EFIAPI
OpenProtocol (
IN EFI_HANDLE Handle,
IN EFI_GUID *Protocol,
OUT VOID **Interface OPTIONAL,
IN EFI_HANDLE AgentHandle,
IN EFI_HANDLE ControllerHandle,
IN UINT32 Attributes
);
访问属性(Attributes)
| 属性常量 | 含义 |
|---|---|
EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL |
直接获取协议接口,不跟踪打开记录(类似旧版 HandleProtocol) |
EFI_OPEN_PROTOCOL_GET_PROTOCOL |
获取协议接口,但不增加打开计数 |
EFI_OPEN_PROTOCOL_TEST_PROTOCOL |
仅测试协议是否存在,不返回接口 |
EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER |
子控制器打开父控制器上的协议 |
EFI_OPEN_PROTOCOL_BY_DRIVER |
驱动打开协议,用于驱动绑定模型 |
EFI_OPEN_PROTOCOL_EXCLUSIVE |
排他性打开,阻止其它非 BY_DRIVER 打开 |
CloseProtocol 函数原型
EFI_STATUS
EFIAPI
CloseProtocol (
IN EFI_HANDLE Handle,
IN EFI_GUID *Protocol,
IN EFI_HANDLE AgentHandle,
IN EFI_HANDLE ControllerHandle
);
关闭协议时,必须使用与打开时相同的 AgentHandle 和 ControllerHandle 参数,否则关闭失败。
// 打开协议示例(BY_DRIVER 方式)
EFI_BLOCK_IO_PROTOCOL *BlockIo;
Status = gBS->OpenProtocol (
Controller,
&gEfiBlockIoProtocolGuid,
(VOID **)&BlockIo,
ThisDriver,
Controller,
EFI_OPEN_PROTOCOL_BY_DRIVER
);
// 使用完成后关闭
gBS->CloseProtocol (
Controller,
&gEfiBlockIoProtocolGuid,
ThisDriver,
Controller
);
4.6 协议操作流程总览
下图展示了协议从安装到使用的完整生命周期,以及各 API 在流程中的位置。
4.7 协议操作 API 对比
| API | 角色 | 输入关键参数 | 输出 | 典型场景 |
|---|---|---|---|---|
InstallProtocolInterface |
生产者 | Handle, GUID, Interface | 协议注册到数据库 | 驱动入口安装协议 |
LocateProtocol |
消费者 | GUID | 协议接口指针 | 快速获取第一个匹配协议 |
OpenProtocol |
消费者 | Handle, GUID, AgentHandle, Attributes | 协议接口指针 + 打开记录 | 驱动绑定、排他访问 |
CloseProtocol |
消费者 | Handle, GUID, AgentHandle, ControllerHandle | 释放协议引用 | 驱动卸载、资源清理 |
4.8 协议数据库内部结构示意
UEFI 维护一个全局的协议数据库,其核心结构可以抽象为“句柄-协议”映射表。每个句柄可以挂载多个协议,每个协议通过 GUID 唯一标识。
LocateProtocol 时,如果系统中存在多个相同 GUID 的协议实例,只会返回第一个。若需要操作特定设备上的协议,应先用 LocateHandleBuffer 枚举句柄,再对每个句柄调用 OpenProtocol。
4.9 最佳实践与避坑指南
- 优先使用 OpenProtocol/CloseProtocol:相比
LocateProtocol,OpenProtocol提供了属性控制和打开记录追踪,更适合驱动绑定模型。 - 匹配打开与关闭参数:
CloseProtocol的AgentHandle和ControllerHandle必须与OpenProtocol时完全一致。 - 检查返回值:所有协议操作 API 都应检查返回的
EFI_STATUS,特别是EFI_UNSUPPORTED和EFI_NOT_FOUND。 - 避免重复安装:在同一个句柄上重复安装相同 GUID 的协议会导致未定义行为,建议先检查是否已存在。
- 协议接口生命周期:确保协议接口内存在
CloseProtocol之前保持有效,驱动卸载时应关闭所有已打开的协议。