UEFI Protocol 是 UEFI 固件中定义的一套标准硬件接口规范,相当于为电脑各个部件制定的"通用语言"。比如:
- 开机画面显示:系统通过 Graphics Output Protocol 与显卡对话,就向显示器通过 HDMI 接口接收信号。
- 键盘输入检测:Simple Input Protocol 让系统识别键盘按键,就向 USB 接口接收键盘输入。
- 硬盘数据读取:Block I/O Protocol 提供访问硬盘的能力,就向 SATA 接口连接硬盘。
- 网络连接:TCP/IP Protocol 实现网络通信,就向网卡通过 RJ45 接口联网。
这些 Protocol 就像电脑主板上的各种标准化接口(USB/HDMI/SATA等),让不同厂商的硬件都能即插即用,不需要为每个设备单独开发驱动。
1. 引言
1.1 UEFI Protocol概述
UEFI Protocol是UEFI规范定义的核心接口机制,为预启动环境中的硬件抽象和组件交互提供标准化方法。与Linux设备模型相比:
特性 | UEFI Protocol | Linux设备模型 |
---|---|---|
设计目标 | 预启动环境硬件抽象 | 操作系统运行时设备管理 |
接口形式 | 基于GUID的C语言接口 | 基于文件系统的设备节点 |
发现机制 | Handle Database | sysfs和设备树 |
权限控制 | 简单权限模型 | 复杂权限系统 |
2. Protocol核心知识点
每个Protocol都包含了三部分内容:GUID、Protocol接口的结构体和Protocol服务(即Protocol的接口函数),使用结构体的方式,使用函数栀指针作为结构体的成员变量,提供给用户使用。
- GUID:全局唯一标识符(如串口Protocol的EFI_SERIAL_IO_PROTOCOL_GUID)
- 结构体:包含函数指针和数据的接口定义(如EFI_SERIAL_IO_PROTOCOL
- 服务函数:结构体中的函数指针成员(如Read/Write)
例如在MdePkg/Include/Protocol/SerialIo.h
文件中就是一个串口的 Protocol
实例,代码清单如下:
#define EFI_SERIAL_IO_PROTOCOL_GUID \
{ \
0xBB25CF6F, 0xF1D4, 0x11D2, {0x9A, 0x0C, 0x00, 0x90, 0x27, 0x3F, 0xC1, 0xFD } \
}
struct _EFI_SERIAL_IO_PROTOCOL {
UINT32 Revision;
EFI_SERIAL_RESET Reset;
EFI_SERIAL_SET_ATTRIBUTES SetAttributes;
EFI_SERIAL_SET_CONTROL_BITS SetControl;
EFI_SERIAL_GET_CONTROL_BITS GetControl;
EFI_SERIAL_WRITE Write;
EFI_SERIAL_READ Read;
EFI_SERIAL_IO_MODE *Mode;
CONST EFI_GUID *DeviceTypeGuid; // Revision 1.1
};
从结构体里面可以看出,结构体具有包括写串口、读串口、设置串口模式等函数,这些都是函数指针,已其中的成员变量Read
来进行举例,代码清代如下:
EFI_STATUS
EFIAPI
SerialRead (
IN EFI_SERIAL_IO_PROTOCOL *This,
IN OUT UINTN *BufferSize,
OUT VOID *Buffer
);
在SerialRead
函数中的this是指向EFI_SERIAL_IO_PROTOCOL
的this指针。所有的Protocol的第一个参数都是指向Protocol实例的指针。例如UEFI串口在访问的时候,与哪一个串口进行通行,就是使用这个参数来决定。这里的this
指针需要程序员自己手动添加。
3. 如何使用Protocol
使用Protocol一般需要有三个步骤:
- 找到Protocol实例;
- 使用这个Protocol提供的接口函数来实现所需要的功能;
- 最后使用
CloseProtocol
关闭打开的Protocol实例。
3.1 打开服务的方法
3.1.1 OpenProtocol()函数
OpenProtocol()是UEFI Boot Services提供的关键函数,用于获取特定设备句柄上的Protocol接口并建立使用关系。该函数的主要功能包括:
- 功能定位:通过指定GUID和句柄,获取对应的Protocol接口实例;
- 权限管理:控制对Protocol的访问权限,确保安全使用
- 引用计数:维护Protocol的使用计数,防止资源冲突
typedef
EFI_STATUS
(EFIAPI *EFI_OPEN_PROTOCOL)(
IN EFI_HANDLE Handle,
IN EFI_GUID *Protocol,
OUT VOID **Interface OPTIONAL,
IN EFI_HANDLE AgentHandle,
IN EFI_HANDLE ControllerHandle,
IN UINT32 Attributes
);
参数说明:
- Handle:目标设备句柄
- Protocol:要打开的Protocol的GUID
- Interface:返回的Protocol接口指针
- AgentHandle:调用者标识(通常是ImageHandle)
- ControllerHandle:控制器句柄(可选)
- Attributes:打开属性标志,控制访问方式
常用属性标志:
- EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL:标准打开方式
- EFI_OPEN_PROTOCOL_GET_PROTOCOL:仅获取接口
- EFI_OPEN_PROTOCOL_EXCLUSIVE:独占访问
如果Handle设备的链表中有这个Protocol,则Protocol实例的指针将写到*Interface中。
3.1.2 HandleProtocol()函数
HandleProtocol()是UEFI Boot Services提供的Protocol获取函数,用于从指定设备句柄上直接获取Protocol接口实例。
主要特点:
- 直接访问:通过已知设备句柄快速获取Protocol
- 简单易用:无需处理复杂的权限属性
- 无引用计数:不增加Protocol的使用计数
函数原型:
typedef
EFI_STATUS
(EFIAPI *EFI_HANDLE_PROTOCOL)(
IN EFI_HANDLE Handle,
IN EFI_GUID *Protocol,
OUT VOID **Interface
);
参数说明:
- Handle:目标设备句柄
- Protocol:要获取的Protocol的GUID
- Interface:返回的Protocol接口指针
返回值:
- EFI_SUCCESS:成功获取Protocol
- EFI_UNSUPPORTED:句柄不支持该Protocol
- EFI_INVALID_PARAMETER:参数无效
3.1.3 LocateProtocol()函数
LocateProtocol()是UEFI Boot Services提供的全局Protocol查找函数,用于在系统中查找第一个匹配指定GUID的Protocol实例。
主要特点:
- 全局查找:搜索整个系统已安装的Protocol
- 快速访问:无需知道具体设备句柄
- 返回首个匹配:只返回第一个找到的Protocol实例
函数原型:
typedef
EFI_STATUS
(EFIAPI *EFI_LOCATE_PROTOCOL)(
IN EFI_GUID *Protocol,
IN VOID *Registration OPTIONAL,
OUT VOID **Interface
);
参数说明:
- Protocol:要查找的Protocol的GUID
- Registration:可选注册键(通常为NULL)
- Interface:返回的Protocol接口指针
返回值:
- EFI_SUCCESS:成功找到Protocol
- EFI_NOT_FOUND:未找到匹配的Protocol
- EFI_INVALID_PARAMETER:参数无效
特性 | OpenProtocol | HandleProtocol | LocateProtocol |
---|---|---|---|
获取方式 | 通过句柄+属性 | 直接通过句柄 | 全局搜索 |
权限控制 | 支持(Attributes) | 不支持 | 不支持 |
引用计数 | 维护 | 不维护 | 不维护 |
使用场景 | 需要精细控制时 | 已知句柄快速访问 | 查找全局Protocol |
性能消耗 | 中等 | 低 | 高(需遍历) |
选择建议:
- 需要精细控制时使用OpenProtocol(),在UEFI规范中,建议使用OpenProtocol。
- 已知具体句柄时使用HandleProtocol()
- 仅需查找任意实例时使用LocateProtocol()
对于Protocol的查找都是通过一个Handle和Protocol关系图来进行的,但是现在有点搞不懂,就先不写了,后面搞懂了在写,这是教材上面的一张图。
4.代码实例
代码源于《UEFI编程实践》这本书上面,代码说明:
- 使用
LocateHandleBuffer()
函数,得到指定Protocol GUID的所有设备句柄。 - 通过
HandleProtocol()
函数得到最先扫描的设备句柄的Protocol指针。 - 通过
OpenProtocolInformation()
函获取最先扫描的设备句柄的Protocol信息。 - 调用
CloseProtocol()
函数关闭Protocol,并回收被调用函数申请的内存。
4.1 使用Protocol的通用模板:
#include <Uefi.h>
#include <Library/UefiBootServicesTableLib.h>
/**
* 通用 Protocol 使用函数
*
* @param ProtocolGuid 要使用的 Protocol 的 GUID
* @param ProtocolInstance 返回的 Protocol 实例指针
* @param Handle 可选,指定要从哪个设备句柄获取 Protocol
*
* @return EFI_STATUS
*/
EFI_STATUS UseGenericProtocol(
IN EFI_GUID *ProtocolGuid,
OUT VOID **ProtocolInstance,
IN EFI_HANDLE Handle OPTIONAL
)
{
EFI_STATUS Status;
if (ProtocolGuid == NULL || ProtocolInstance == NULL) {
return EFI_INVALID_PARAMETER;
}
// 方法1: 如果有特定句柄,使用 OpenProtocol
if (Handle != NULL) {
Status = gBS->OpenProtocol(
Handle,
ProtocolGuid,
ProtocolInstance,
gImageHandle, // 使用当前镜像的句柄
NULL,
EFI_OPEN_PROTOCOL_GET_PROTOCOL
);
}
// 方法2: 没有特定句柄,使用 LocateProtocol
else {
Status = gBS->LocateProtocol(
ProtocolGuid,
NULL,
ProtocolInstance
);
}
if (EFI_ERROR(Status)) {
DEBUG((EFI_D_ERROR, "Failed to get protocol: %r\n", Status));
return Status;
}
if (*ProtocolInstance == NULL) {
DEBUG((EFI_D_ERROR, "Protocol instance is NULL\n"));
return EFI_NOT_FOUND;
}
return EFI_SUCCESS;
}
/**
* 使用 Protocol 的示例
*/
EFI_STATUS ExampleProtocolUsage()
{
EFI_STATUS Status;
VOID *Protocol = NULL;
// 1. 获取 Protocol 实例
Status = UseGenericProtocol(
&gEfiSomeProtocolGuid, // 替换为实际的 Protocol GUID
&Protocol,
NULL // 不指定特定句柄
);
if (EFI_ERROR(Status)) {
return Status;
}
// 2. 将通用指针转换为具体 Protocol 类型
EFI_SOME_PROTOCOL *SomeProtocol = (EFI_SOME_PROTOCOL *)Protocol;
// 3. 使用 Protocol 提供的方法
// 例如: SomeProtocol->SomeFunction(SomeProtocol, ...);
return EFI_SUCCESS;
}
4.2 ProtocolStudy.c
#include "ProtocolStudy.h"
/**
* @brief 列出指定Protocol的信息并获取接口实例
* @param ProtocolGuid 要查询的Protocol的GUID
* @param Interface 返回获取到的Protocol接口指针
* @return EFI_STATUS 执行状态码
*/
EFI_STATUS ListProtocolMsg(IN EFI_GUID *ProtocolGuid, OUT VOID **Interface)
{
EFI_STATUS Status;
EFI_HANDLE *myHandleBuff = NULL; // 存储找到的设备句柄数组
UINTN i, HandleCount = 0; // 句柄计数器和总数
EFI_OPEN_PROTOCOL_INFORMATION_ENTRY *InfEnterArray; // Protocol打开信息数组
UINTN InfEnterCount; // Protocol打开信息条目数
// 打印Protocol的GUID信息(格式:{Data1,Data2,Data3 Data4})
Print(L"GUID:{0x%08x,0x%04x,%0x04x}", ProtocolGuid->Data1, ProtocolGuid->Data2, ProtocolGuid->Data3);
for (UINT8 i = 0; i < 8; i++)
{
Print(L" 0x%02x", ProtocolGuid->Data4[i]); // 打印GUID的Data4部分
}
Print(L"\n");
// 步骤1:通过Protocol GUID查找所有支持该Protocol的设备句柄
Status = gBS->LocateHandleBuffer(
ByProtocol, // 按Protocol类型查找
ProtocolGuid, // 目标Protocol的GUID
NULL, // 无额外搜索条件
&HandleCount, // 返回找到的句柄数量
&myHandleBuff // 返回句柄数组指针
);
if (EFI_ERROR(Status))
{
Print(L"Not Found Handle\n"); // 未找到任何支持该Protocol的句柄
return Status;
}
else
{
Print(L"Found Handle Count:%d\n", HandleCount); // 打印找到的句柄数量
}
// 步骤2:遍历找到的句柄,尝试获取Protocol接口
for (i = 0; i < HandleCount; i++)
{
// 尝试从当前句柄获取Protocol接口
Status = gBS->HandleProtocol(
myHandleBuff[i], // 当前设备句柄
ProtocolGuid, // Protocol的GUID
Interface // 返回的Protocol接口指针
);
if (!EFI_ERROR(Status))
{
break; // 成功获取到接口,退出循环
}
}
// 步骤3:获取并打印Protocol的详细信息
Status = gBS->OpenProtocolInformation(
myHandleBuff[i], // 目标设备句柄
ProtocolGuid, // Protocol的GUID
&InfEnterArray, // 返回打开信息数组
&InfEnterCount // 返回信息条目数
);
if (EFI_ERROR(Status))
{
Print(L"NOT GET the Protocol's information!\n"); // 获取信息失败
}
else
{
Print(L"EntryCount = %d \n", InfEnterCount); // 打印打开信息条目数
// 关闭Protocol(释放资源)
gBS->CloseProtocol(
myHandleBuff[i], // 设备句柄
ProtocolGuid, // Protocol的GUID
InfEnterArray->AgentHandle, // 调用者句柄
InfEnterArray->ControllerHandle // 控制器句柄
);
if (InfEnterArray)
{
gBS->FreePool(InfEnterArray); // 释放打开信息数组内存
}
}
// 释放句柄数组内存
if (myHandleBuff)
{
gBS->FreePool(myHandleBuff);
}
return Status;
}
/**
* @brief UEFI应用程序入口函数
* @param ImageHandle 应用程序镜像句柄
* @param SystemTable 系统表指针
* @return EFI_STATUS 执行状态码
*/
EFI_STATUS EFIAPI UefiMain(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable)
{
EFI_RNG_PROTOCOL *gRNGOut; // 随机数Protocol接口指针
EFI_SERIAL_IO_PROTOCOL *gSerialIO; // 串口Protocol接口指针
// 设置控制台输出颜色为红底黑字
gST->ConOut->SetAttribute(gST->ConOut, EFI_BACKGROUND_BLACK | EFI_RED);
// 查找并列出串口Protocol信息
Print(L"Action:SerialProtocol\n");
ListProtocolMsg(&gEfiSerialIoProtocolGuid, (VOID **)&gSerialIO);
// 查找并列出随机数Protocol信息
Print(L"Action:RngProtocol\n");
ListProtocolMsg(&gEfiRngProtocolGuid, (VOID **)&gRNGOut);
// 恢复控制台默认颜色(黑底灰字)
gST->ConOut->SetAttribute(gST->ConOut, EFI_BACKGROUND_BLACK | EFI_LIGHTGRAY);
return EFI_SUCCESS; // 返回成功状态
}
4.3 ProtocolStudy.h
#ifndef __PROTOCOLSTUDY_H
#define __PROTOCOLSTUDY_H
#include <Uefi.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/UefiLib.h>
#include <Protocol/Rng.h>
#include <Protocol/SerialIo.h>
#include <Library/UefiBootServicesTableLib.h>
#endif
4.4 ProtocolStudy.inf
[Defines]
INF_VERSION = 0x00010006
BASE_NAME = ProtocolStudy
FILE_GUID = 31e990f8-918c-45db-9736-9205126eaa83
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 1.0
ENTRY_POINT = UefiMain
[Sources]
ProtocolStudy.c
[Packages]
MdePkg/MdePkg.dec
[LibraryClasses]
UefiApplicationEntryPoint
UefiLib
UefiBootServicesTableLib
[Protocols]
gEfiSerialIoProtocolGuid # 声明依赖的SerialIo协议
gEfiRngProtocolGuid # 声明依赖的RNG协议