UEFI的Protocol
本文为参阅UEFI原理与编程第四章及他人博客后写的融合怪,由于UEFI spec第八章起都是Protocol的,觉得有必要写,侵删
Protocol的概念
在计算机通信中,Protocol是网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定,是为了使数据在网络上从源到达目的,网络通信的参与方必须遵循相同的规则,这套规则称为协议(protocol),典型的Protocol有HTTP、FTP、TCP、IP等,通过Http完成了文件的传输。
在UEFI中Protocol同样是重要的概念之一,Protocol提供了一种在UEFI应用程序以及UEFI驱动之间的通信方式。通过Protocol,用户可以使用驱动提供的服务,以及系统提供的其他服务。从本质上说是一种调用者与被调用者之间的“约定”。而这种“约定”在软件开发领域有另一个更形象化的名字叫接口(Interface)。为了做到二进制间的互操作,那么参与操作的双方(调用者与被调用者)都必须做出一定的让步,这个让步就是双方必须遵循实现商量好的调用方法(接口),而这种事先约定的接口就是protocol的定义。Protocol引入了C++的面向对象的思想来设计管理,相当于C++的class,用 struct 来模拟 class,用函数指针(Protocol的成员变量)模拟成员函数,此种函数的第一参数必须是指向Protocol的指针,用来模拟this指针。
DXE驱动之间通过Protocol通信,Protocol是一种特殊的结构体,每个Protocol对应一个GUID,利用系统BootService的OpenProtocol,并根据GUID来打开对应的protocol,进而使用这个Protocol提供的服务。Protocol不是UEFI BIOS一开始就可以用的。UEFI BIOS启动时分为不同的阶段,SEC-PEI-DXE-BDS等等。而Protocol需要等到DXE阶段才可以使用(不需要特别在意DXE阶段的哪个点开始,基本上开发时写的DXE模块都可以使用)。UEFI框架下提供了函数来存取Protocol,大部分的设备初始化和其它功能代码也都被包装成了一个个的Protocol。Protocol的作用跟普通的结构体没有区别,如果存放的是数据就作为存储用,如果存放的是函数指针就用作特定代码执行。
Protocol的数据结构
Protocol不是很复杂的东西,直观来说,它就是一个结构体, Protocol的作用跟普通的结构体没有区别,如果存放的是数据就作为存储用,如果存放的是函数指针就用作特定代码执行, 同时 UEFI框架下提供了函数来存取Protocol。UEFI下将大部分的设备初始化和其它功能代码都包装成了一个个的Protocol,比如说下面是一个用于存储设备访问的Protocol:
// @f ile MdePkg/Include/Protocol/Blocklo .h
/// 通过这个Protocol可以控制块设备
struct _EFI_BLOCK_IO_PROTOCOL {
///
///Protocol版本号,Protocol必须保证向后兼容
///如果没有向后兼容,必须给未来的版本定义不同的GUID,也就是必须定义一个不同的Protocol
///
UINT64 Revision;
///
/// Pointer to the EFI_BLOCK_IO_MEDIA data for this device.
///
EFI_BLOCK_IO_MEDIA *Media; //指针指向这个设备
EFI_BLOCK_RESET Reset; //重置复位信号
EFI_BLOCK_READ ReadBlocks; //读Protocol服务
EFI_BLOCK_WRITE WriteBlocks; //写Protocol服务
EFI_BLOCK_FLUSH FlushBlocks; //清除缓存服务
};
extern EFI_GUID gEfiBlockloProtocolGuid; //导出该Protocol
每个Protocol必须有一个唯一的GUID,例如在 Blocklo.h 中定义了 Blocklo 的 GUID,如下所示:
#define EFI_BLOCK_IO_PROTOCOL_GUID\
{\
0x964e5b21, 0x6459, 0xlld2, {0x8e, 0x39,0x0, OxaO, 0xc9, 0x69, 0x72, 0x3b }\
}
typedef struct _EFI_BLOCK_IO_PROTOCOL EFI_BLOCK_IO_PROTOCOL;
结构体EFI_BLOCK_IO_PROTOCOL有两个成员变量和4个成员函数(当然,从C语言的角度来看,“成员函数”这样的叫法不准确,它实际上也是一个成员变量,只是这个变量是函数指针而已)。gEfiBlockIoProtocolGuid ({ 0x964e5b21, 0x6459,0x11d2,{ 0x8e, 0x39, 0x0, 0xa0, 0xc9, 0x69, 0x72, 0x3b} })是一种标示符,标示了 EFI_BLOCK_IO_PROTOCOL。
下面是 EFI_BLOCK_IO_PROTOCOL的ReadBlocks服务的函数原型:
/**
从地址Lba开始的块读取Buffersize字节到缓冲区
@retval EFI_SUCCESS 数据从设备正确读出
@retval EFI_DEVICE_ERROR 设备出现错误
@retval EFI_NO_MEDIA 设备中没有介质
@retval EFI_MEDIA_CHANGED Mediald与当前设备不符
@retval EFI_BAD_BUFFER_SIZE 缓冲区大小不是块的整数倍
@retval EFI_INVALID_PARAMETER 要读取的块中包含无效块;或缓冲区未对齐
**/
typedef EFI_STATUS(EFIAPI *EFI_BLOCK_READ)(
IN EFI_BLOCK_IO_PROTOCOL *This, //This 指针,指向调用上下文
IN UINT32 Mediald, // media Id
IN EFI_LBA Lba, //要读取的启始块逻辑地址
IN UINTN BufferSize, //要读取的字节数,必须是块大小的整数倍
OUT VOID *Buffer //目的缓冲区,调用者负责该缓冲区的创建与删除
}
它的第一个参数,指向EFI_BLOCK_IO_PROTOCOL对象自己的This指针,这是成员函数区别于一般函数的重要特征。通常,计算机中有许多不同的块设备,每个块设备都有一个EFI_BLOCK_IO_PROTOCOL的实例,This指针就是指向这个实例,用于告诉成员函数我们正在操作哪个设备。This指针是Protocol成员函数的一个重要特征,与C++成员函数this指针的区别是,C++的this指针由编译器自动加人,而Protocol成员函数的This指针需手工添加。
Protocol的实现
这里需要关注的图中红框部分的内容。
这里实际上是两种链表,一种是Handle的链表,一种是Protocol的链表。Handle其实就是一个不会重复的整型数字,而Protocol在之前已经说过就是一个结构体。各个Handle连接在一起构成一个链表,每个Handle上可以附着若干个不会重复的Protocol。
上述的两种链表交织成了一张网,这张网被称为“Handle Database”。
这个Handle Database会在DXE最开始的地方初始化起来,之后通过接口可以扩展,搜寻等等操作。
下面认识一下EFI_HANDLE。typedef VOID *EFI_HANDLE;
EFI_HANDLE是指向某种对象的指针,UEFI用它来表示某个对象。UEFI扫描总线后,会为每个设备建立一个Controller对象,用于控制设备,所有该设备的驱动以Protocol的形式安装到这个Controller中,这个Controller就是一个EFI_HANDLE对象。当我们将一个.efi文件加载到内存中时,UEFI也会为该文件建立一个Image对象(此Image非图像的意思),这个Image对象也是一个EFI_HANDLE对象。在UEFI内部,EFI_HANDLE被理解为IHANDLE, IHANDLE的数据结构如代码所示。
/// IHANDLE -包含了 Protocols 链表
typedef struct {
UINTN Signature; //表明Handle的类别
LIST_ENTRY AllHandles; //所有IHANDLE组成的链表
LIST_ENTRY Protocols; //此 Handle 的 Protocols 链表
UINTN LocateRequest;
UINT64 Key;
} IHANDLE;
每 个 IHANDLE中 都 有 一 个Protocol链 表 , 存 放 属 于 自 己 的 Protocol。所 有 的IHANDLE通过AllHandles链接起来。上图展示了 IHANDLE内的Protocol是如何被组织起来的。IHANDLE的Protocols是一个双向链表,链表中每一个元素是PROTOCOL_INTERFACE ,通过 PROTOCOL_INTERFACE 的 Protocol 指针可以得到这个 Protocol 的GUID,通过Interface指针可以得到这个Protocol的实例。
Protocol的使用
在UEFI Boot Service中提供了如下的函数用来操作Protocol:
Name | Type | Description |
---|---|---|
InstallProtocolInterface | Boot | 在设备句柄上安装protocol接口 |
UninstallProtocolInterface | Boot | 从设备句柄中移除protocol接口 |
ReinstallProtocolInterface | Boot | 在设备句柄上重新安装protocol接口。 |
RegisterProtocolNotify | Boot | 注册一个事件,该事件在为指定protocol安装接口时发出信号。 |
LocateHandle | Boot | 返回支持指定protocol的句柄数组。 |
HandleProtocol | Boot | 查询句柄以确定它是否支持指定的protocol。 |
LocateDevicePath | Boot | 在支持指定protocol的设备路径上定位所有设备,并返回最接近该路径的设备的句柄。 |
OpenProtocol | Boot | 向使用protocol接口的代理列表中添加元素 |
CloseProtocol | Boot | 从使用protocol接口的代理列表中删除元素。 |
OpenProtocolInformation | Boot | 检索当前正在使用protocol接口的代理的列表。 |
ConnectController | Boot | 使用一组优先规则来找到管理控制器的最佳驱动程序集。 |
DisconnectController | Boot | 通知一组驱动程序停止管理控制器。 |
ProtocolsPerHandle | Boot | 检索安装在句柄上的protocol列表。返回缓冲区被自动分配 |
LocateHandleBuffer | Boot | 从符合搜索条件的句柄数据库中检索句柄列表。返回缓冲区被自动分配。 |
LocateProtocol | Boot | 找到句柄数据库中支持请求protocol的第一个句柄。 |
InstallMultipleProtocolInterfaces | Boot | 将一个或多个protocol接口安装到句柄上。 |
UninstallMultipleProtocolInterfaces | Boot | 从句柄中卸载一个或多个protocol接口 |
具体可以查看UEFI Spec 6.3节
它们可以分为几种不同的类型:
-
安装和卸载接口,就是这里的IntallXXX,ReinstallXXX,UninstallXXX,IntallMultipleXXX,UninstallMultipleXXX等。
-
获取和关闭接口,比如HandleProtocol,LocateHandle等等。
-
其它辅助接口,比如OpenProtocolInformation,RegisterProtocolNotify等,其中RegisterProtocolNotify注册了一个回调函数,当指定的Protocol被安装时,这个回调函数就会被执行
其他类别的Protocol
参考博客:https://blog.youkuaiyun.com/jiangwei0512/article/details/86996846
UEFI中的Protocol有一些比较特殊的类型,本节将介绍这些Protocol。
Architectural Protocol
UEFI规定了一些Protocol,这些Protocol在UEFI BIOS运行的过程中会安装,且一定需要被安装,如果没有被安装的话,系统就会报错。
这些Protocol如下所示:
//
// DXE Core Global Variables for all of the Architectural Protocols.
// If a protocol is installed mArchProtocols[].Present will be TRUE.
//
// CoreNotifyOnArchProtocolInstallation () fills in mArchProtocols[].Event
// and mArchProtocols[].Registration as it creates events for every array
// entry.
//
EFI_CORE_PROTOCOL_NOTIFY_ENTRY mArchProtocols[] = {
{ &gEfiSecurityArchProtocolGuid, (VOID **)&gSecurity, NULL, NULL, FALSE },
{ &gEfiCpuArchProtocolGuid, (VOID **)&gCpu, NULL, NULL, FALSE },
{ &gEfiMetronomeArchProtocolGuid, (VOID **)&gMetronome, NULL, NULL, FALSE },
{ &gEfiTimerArchProtocolGuid, (VOID **)&gTimer, NULL, NULL, FALSE },
{ &gEfiBdsArchProtocolGuid, (VOID **)&gBds, NULL, NULL, FALSE },
{ &gEfiWatchdogTimerArchProtocolGuid, (VOID **)&gWatchdogTimer, NULL, NULL, FALSE },
{ &gEfiRuntimeArchProtocolGuid, (VOID **)&gRuntime, NULL, NULL, FALSE },
{ &gEfiVariableArchProtocolGuid, (VOID **)NULL, NULL, NULL, FALSE },
{ &gEfiVariableWriteArchProtocolGuid, (VOID **)NULL, NULL, NULL, FALSE },
{ &gEfiCapsuleArchProtocolGuid, (VOID **)NULL, NULL, NULL, FALSE },
{ &gEfiMonotonicCounterArchProtocolGuid, (VOID **)NULL, NULL, NULL, FALSE },
{ &gEfiResetArchProtocolGuid, (VOID **)NULL, NULL, NULL, FALSE },
{ &gEfiRealTimeClockArchProtocolGuid, (VOID **)NULL, NULL, NULL, FALSE },
{ NULL, (VOID **)NULL, NULL, NULL, FALSE }
};
这些Protocol都是UEFI或者系统必须的最基础的Protocol,比如说这里的gEfiBdsArchProtocolGuid对应的Protocol,它是BDS阶段的如果,在DXEMain.c中有如下的代码:
//
// Transfer control to the BDS Architectural Protocol
//
gBds->Entry (gBds);
使DXE阶段过渡到BDS阶段。
Device Path Protocol
Device Path Protocol是一种纯数据的结构体,它表示的是一个设备的可编程路径,可以简称就是Device Path(后面就直接省略掉Protocol)。这种说法比较抽象,而且这里说的“设备”也并不一定需要是真实的设备,它可以是虚拟设备,甚至可以是一个文件。
Device Path的具体说明有在其它的文章中介绍,这里不做具体的说明。
简单介绍一下它的结构体:
/**
此协议可用于任何设备句柄,以获取有关物理设备或逻辑设备的通用路径/位置信息。
如果句柄在逻辑上没有映射到物理设备,则句柄可能不一定支持设备路径协议。
设备路径描述了句柄所对应的设备的位置。设备路径的大小可以从构成设备路径的结构中确定。
**/
typedef struct {
UINT8 Type; ///< 0x01 Hardware Device Path.
///< 0x02 ACPI Device Path.
///< 0x03 Messaging Device Path.
///< 0x04 Media Device Path.
///< 0x05 BIOS Boot Specification Device Path.
///< 0x7F End of Hardware Device Path.
UINT8 SubType; ///< Varies by Type
///< 0xFF End Entire Device Path, or
///< 0x01 End This Instance of a Device Path and start a new
///< Device Path.
UINT8 Length[2]; ///< Specific Device Path data. Type and Sub-Type define
///< type of data. Size of data is included in Length.
} EFI_DEVICE_PATH_PROTOCOL;
它的结构非常的简单,是一个可变长的结构体。成员包括了一个基本的头部(分为类型,子类型和长度三部分),以及之后的具体类型所需要包含的成员。Device Path有一个非常重要的作用就是标记对应Handle的属性。
举一个简单的例子,现在有两个硬盘,那么它们都有一个_EFI_BLOCK_IO_PROTOCOL(见开头),然而我们想访问其中一个特定的硬盘,如何找到这个硬件,就可以依赖于Device Path。
以硬盘的Device Path举例,它的类型是HARDWARE_DEVICE_PATH,子类型是HW_CONTROLLER_DP,因此它的Device Path中包含如下的部分:
///
/// Controller Device Path.
///
typedef struct {
EFI_DEVICE_PATH_PROTOCOL Header;
///
/// Controller number.
///
UINT32 ControllerNumber;
} CONTROLLER_DEVICE_PATH;
而两个不同的硬盘,其中的ControllerNumber可能是不同的(根据不同的硬件配置),因此就可以确定到底使用哪个Device Path,最终获取到正确的_EFI_BLOCK_IO_PROTOCOL,大致流程如下:
- 调用LocateHandleBuffer获取到所有安装了_EFI_BLOCK_IO_PROTOCOL的Handle;
- 遍历所有的Handle;
- 根据上述Handle获取到Device Path Protocol,根据这个Device Path Protocol就能够确定该Handle是否是我们要找的那个Handle;
- 通过找到的Handle,调用HandleProtocol来到对应的_EFI_BLOCK_IO_PROTOCOL,然后就可以使用这个Protocol来访问硬盘。
以上是使用Device Path Protocol的一个示例,当然Device Path Protocol的用法还有很多,可以参考UEFI Spec第九章。