BIOS知识枝桠—— 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:

NameTypeDescription
InstallProtocolInterfaceBoot在设备句柄上安装protocol接口
UninstallProtocolInterfaceBoot从设备句柄中移除protocol接口
ReinstallProtocolInterfaceBoot在设备句柄上重新安装protocol接口。
RegisterProtocolNotifyBoot注册一个事件,该事件在为指定protocol安装接口时发出信号。
LocateHandleBoot返回支持指定protocol的句柄数组。
HandleProtocolBoot查询句柄以确定它是否支持指定的protocol。
LocateDevicePathBoot在支持指定protocol的设备路径上定位所有设备,并返回最接近该路径的设备的句柄。
OpenProtocolBoot向使用protocol接口的代理列表中添加元素
CloseProtocolBoot从使用protocol接口的代理列表中删除元素。
OpenProtocolInformationBoot检索当前正在使用protocol接口的代理的列表。
ConnectControllerBoot使用一组优先规则来找到管理控制器的最佳驱动程序集。
DisconnectControllerBoot通知一组驱动程序停止管理控制器。
ProtocolsPerHandleBoot检索安装在句柄上的protocol列表。返回缓冲区被自动分配
LocateHandleBufferBoot从符合搜索条件的句柄数据库中检索句柄列表。返回缓冲区被自动分配。
LocateProtocolBoot找到句柄数据库中支持请求protocol的第一个句柄。
InstallMultipleProtocolInterfacesBoot将一个或多个protocol接口安装到句柄上。
UninstallMultipleProtocolInterfacesBoot从句柄中卸载一个或多个protocol接口

具体可以查看UEFI Spec 6.3节
它们可以分为几种不同的类型:

  1. 安装和卸载接口,就是这里的IntallXXX,ReinstallXXX,UninstallXXX,IntallMultipleXXX,UninstallMultipleXXX等。

  2. 获取和关闭接口,比如HandleProtocol,LocateHandle等等。

  3. 其它辅助接口,比如OpenProtocolInformation,RegisterProtocolNotify等,其中RegisterProtocolNotify注册了一个回调函数,当指定的Protocol被安装时,这个回调函数就会被执行

其他类别的Protocol

参考博客:https://blog.csdn.net/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,大致流程如下:

  1. 调用LocateHandleBuffer获取到所有安装了_EFI_BLOCK_IO_PROTOCOL的Handle;
  2. 遍历所有的Handle;
  3. 根据上述Handle获取到Device Path Protocol,根据这个Device Path Protocol就能够确定该Handle是否是我们要找的那个Handle;
  4. 通过找到的Handle,调用HandleProtocol来到对应的_EFI_BLOCK_IO_PROTOCOL,然后就可以使用这个Protocol来访问硬盘。

以上是使用Device Path Protocol的一个示例,当然Device Path Protocol的用法还有很多,可以参考UEFI Spec第九章。

EFI Driver Binding Protocol

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值