UEFI协议与平台服务开发工程 - 第4节

4.1 协议的核心概念

在 UEFI 固件体系中,协议(Protocol)是驱动与应用程序之间进行交互的核心抽象。一个协议本质上是一个由函数指针(接口方法)和数据结构组成的 接口契约。驱动通过安装协议来对外提供能力,应用程序或其它驱动则通过定位协议来使用这些能力。

协议的设计遵循面向对象思想:每个协议都有一个唯一的 GUID 作为标识,协议接口通常以 EFI_XXX_PROTOCOL 结构体形式定义,内部包含一组 EFIAPI 回调函数。

关键理解:UEFI 协议是“生产-消费”模型。生产者(驱动)安装协议到句柄数据库,消费者(应用或其它驱动)通过 GUID 查找并打开协议,获得接口函数指针后调用。

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 协议接口结构体实例的指针,即协议函数表

典型安装流程

  1. 驱动入口函数中分配或静态定义一个协议接口实例。
  2. 填充协议结构体中的函数指针,指向驱动内部实现。
  3. 调用 gBS->InstallProtocolInterface(),将协议绑定到句柄。
  4. 系统将协议加入内部数据库,后续可通过 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 在流程中的位置。

UEFI 协议生命周期流程图 1. 定义协议结构 GUID + 函数指针表 2. 安装协议 InstallProtocolInterface 3. 定位协议 LocateProtocol 4. 打开协议 OpenProtocol (带属性) 5. 使用协议接口 调用函数指针 6. 关闭协议 CloseProtocol 说明: LocateProtocol 适用于简单查找;OpenProtocol/CloseProtocol 提供更完善的访问控制,推荐在驱动绑定模型中使用。

4.7 协议操作 API 对比

API 角色 输入关键参数 输出 典型场景
InstallProtocolInterface 生产者 Handle, GUID, Interface 协议注册到数据库 驱动入口安装协议
LocateProtocol 消费者 GUID 协议接口指针 快速获取第一个匹配协议
OpenProtocol 消费者 Handle, GUID, AgentHandle, Attributes 协议接口指针 + 打开记录 驱动绑定、排他访问
CloseProtocol 消费者 Handle, GUID, AgentHandle, ControllerHandle 释放协议引用 驱动卸载、资源清理

4.8 协议数据库内部结构示意

UEFI 维护一个全局的协议数据库,其核心结构可以抽象为“句柄-协议”映射表。每个句柄可以挂载多个协议,每个协议通过 GUID 唯一标识。

协议数据库内部结构示意 Handle 0x3A1F Protocol A (GUID-A) Protocol B (GUID-B) Handle 0x4B2C Protocol A (GUID-A) Protocol C (GUID-C) Handle 0x5C3D Protocol D (GUID-D) 协议数据库(Protocol Database) 关键特征: • 一个句柄可挂载多个不同 GUID 的协议 • 同一个 GUID 的协议可出现在多个句柄上(如 Protocol A 同时存在于 Handle 0x3A1F 和 Handle 0x4B2C)
常见陷阱:使用 LocateProtocol 时,如果系统中存在多个相同 GUID 的协议实例,只会返回第一个。若需要操作特定设备上的协议,应先用 LocateHandleBuffer 枚举句柄,再对每个句柄调用 OpenProtocol。

4.9 最佳实践与避坑指南

  • 优先使用 OpenProtocol/CloseProtocol:相比 LocateProtocol,OpenProtocol 提供了属性控制和打开记录追踪,更适合驱动绑定模型。
  • 匹配打开与关闭参数:CloseProtocol 的 AgentHandle 和 ControllerHandle 必须与 OpenProtocol 时完全一致。
  • 检查返回值:所有协议操作 API 都应检查返回的 EFI_STATUS,特别是 EFI_UNSUPPORTED 和 EFI_NOT_FOUND。
  • 避免重复安装:在同一个句柄上重复安装相同 GUID 的协议会导致未定义行为,建议先检查是否已存在。
  • 协议接口生命周期:确保协议接口内存在 CloseProtocol 之前保持有效,驱动卸载时应关闭所有已打开的协议。