UEFI中handle
protocol
是一个老大难问题了,网上也有不少的前辈对这些概念进行了梳理总结,但是目前我的知识浅薄,还是没有能够领略其中的精髓,特别是将代码和实际应用联系起来十分困难。只不过最近刚刚仔细看了看网上相关的文章并且联系代码学习了一下,所以记录一下当前学习的体会,我是一个百分之百新入门菜鸟,所以我会尽量详细的记录,一是希望能够方便自己复习;二是自己的学习轨迹可能更贴近新人自己学习的方向,希望能够给后来的学习者一点帮助作用。
首先,EDK II UEFI Driver Writer’s Guide 中3.4 Handle database中的部分表述如下:
The handle database is composed of objects called handles and protocols. Handles are a collection of one or more protocols and protocols are data structures named by a GUID. The data structure for a protocol may contain data fields, services, both or none at all.
翻译一下就是handle database 是由handle和protocol组成的,handle就是一个或者更多protocol的集合。有道理又不能理解的样子。。。
其次,根据《UEFI编程与原理》这本书描述
EFI_HANDLE是指向某种对象的指针,UEFI用它来表示某个对象。UEFI扫描总线后,会为每个设备建立一个Controller对象,用于控制设备,所有该设备的驱动以Protocol的形式安装到这个Controller中,这个Controller对象就是一个EFI_HANDLE对象。当我们将一个.efi文件加载到内存中时,UEFI也会为改文件建立一个Image对象,这个Image对象也是一个EFI_HANDLE对象。在UEFI内部,EFI_HANDLE被理解为IHANDLE.
上面一段话比较抽象,到现在我也没有完全透彻的理解,就先捡着我明白的说(吐槽一下 国内的很多书都这样 第一遍读完全不知道什么意思 你只有读几遍甚至几十遍然后自己实践很久之后才能明白他什么意思):
EFI_HANDLE
是一个指针,指向某种对象 EFI_HANDLE
是一个指针,并且这个指针应用十分广泛。广泛到什么程度?
a) UEFI会为每一个连接的设备建立一个EFI_HANDLE
指针指向的对象(Controller)
b) 当一个.efi文件被加载的时候,UEFI也会为这个文件建立一个EFI_HANDLE
指向的对象(ImageHandle)
看到了吗,上面两句话就说明了EFI_HANDLE
的重要程度,Code里面广泛使用的 Controller
和ImageHandle
都是EFI_HANDLE
这个指针指向的对象。
EFI_HANDLE
指向的对象实际上是一个名为IHANDLE
的结构体以上三点就是《UEFI编程与原理》这段话中我总结出的关键点,下面一点点来看
首先看一下Code里面EFI_HANDLE
的定义
//MdePkg\Include\Uefi\UefiBaseType.h
typedef VOID *EFI_HANDLE;
EFI_HANDLE
是一个 void *
类型的数据,在C语言中这表示无明确类型的指针,就是说可以进行任意转换。符合我们前述的类型为指针、应用广泛。
我们知道一个指针最终一定是要指向一段内存进行操作的,而EFI_HANDLE
指向的这段内存就是我们之前提到的对象,这个对象到底是什么样子呢?看下面的一段代码
//摘选自 MdeModulePkg\Core\Dxe\Hand\Handle.c
// CoreInstallProtocolInterfaceNotify 函数
EFI_HANDLE *UserHandle;
IHANDLE *Handle;
Handle = NULL;
// ...
Handle = (IHANDLE *)*UserHandle;
可以发现,最终转换成了 IHANDLE *Handle = (IHANDLE* )EFI_HANDLE
的形式,即指向的对象为IHANDLE
Code 里面关于IHANDLE
的定义
//MdeModulePkg\Core\Dxe\Hand\Handle.h
#define EFI_HANDLE_SIGNATURE SIGNATURE_32('h','n','d','l')
/// IHANDLE - contains a list of protocol handles
typedef struct {
UINTN Signature;
/// All handles list of IHANDLE
LIST_ENTRY AllHandles;
/// List of PROTOCOL_INTERFACE's for this handle
LIST_ENTRY Protocols;
UINTN LocateRequest;
/// The Handle Database Key value when this handle was last created or modified
UINT64 Key;
} IHANDLE;
以上可见IHANDLE
是一个非常标准且常见的结构体形式,我们所说的handle
的实质也就是IHANDLE
结构体。
这这时我们继续看EDK II UEFI Driver Writer’s Guide 中3.4 Handle database中的这部分表述:
The handle database is a list of UEFI handles and is the central repository for the objects maintained by UEFI-based firmware. Each UEFI handle identified by a unique handle number is maintained by the system firmware. A handle number provides a database “key” to an entry in the handle database.
翻译: handle database
是UEFI handle 的链表并且是是由基于 UEFI 的固件维护的对象的核心部分。每个由唯一handle number 编号标识的 UEFI handle 由系统固件维护。 handle number为handle database中的每个entry都提供了“key”。
我们可以将结构体中的 key
理解为每一个handle 的唯一编号,作为查找、区分的标志。通过这段话,就能很好的理解jiangwei0512 博文中的 Handle其实就是一个不会重复的整型数字 这句话的意思了。
观察可以发现结构体中的成员除了常见的UINTN
之外,还有一个特殊的LIST_ENTRY
,这个成员是构成handle&protocol
关系的重要联系,详细了解这个成员的结构对于理解handle&protocol
有着很重要的帮助。
依旧按照习惯先看LIST_ENTRY
定义
//MdePkg\Include\Base.h
typedef struct _LIST_ENTRY LIST_ENTRY;
///
/// _LIST_ENTRY structure definition.
///
struct _LIST_ENTRY {
LIST_ENTRY *ForwardLink;
LIST_ENTRY *BackLink;
};
有一点C语言基础就能够一眼看出,这是一个简单的双向链表节点结构,链表的每一个节点连接的都是LIST_ENTRY
结构体,也就是连接只存在于LIST_ENTRY
结构体之间,而包含了LIST_ENTRY
结构体的IHANDLE
结构就能够通过其成员LIST_ENTRY AllHandles
结构体产生相互连接。换句话说AllHandles
能够形成一个链表,将IHANDLE
按照某种方式串在一起,我们将形成的这个链表就称为Handledatabase
链表(important!!!回收开头)
循着我们刚才的思路,我们发现一个以IHANDLE
的成员 AllHandles
为节点的链表已经构建出来了,问题是这个链表的头是什么样子呢?在Code里面我们能够找到头结点gHandleList
的定义:
//MdeModulePkg\Core\Dxe\Hand\Handle.h
extern LIST_ENTRY gHandleList;
//MdeModulePkg\Core\Dxe\Hand\Handle.c
#define INITIALIZE_LIST_HEAD_VARIABLE(ListHead) {&(ListHead), &(ListHead)}
// gHandleList - A list of all the handles in the system
LIST_ENTRY gHandleList = INITIALIZE_LIST_HEAD_VARIABLE (gHandleList);
//MdePkg\Include\Library\BaseLib.h
#define INITIALIZE_LIST_HEAD_VARIABLE(ListHead) {&(ListHead), &(ListHead)}
注:
//将上面的宏定义展开就能够清楚地的看到初始化的具体形式
LIST_ENTRY gHandleList = {&(ListHead), &(ListHead)}
//这是结构体初始化的一种方式,我本人由于很少使用这种方式,在刚刚看到这段Code的时候确实被唬住了 所以写一段说明防止自己再忘记
//在Code里面也有普通代码形式进行初始化的函数 本质上都是一样的
//MdePkg\Library\BaseLib\LinkedList.c
LIST_ENTRY *
EFIAPI
InitializeListHead (
IN OUT LIST_ENTRY *ListHead
)
{
ASSERT (ListHead != NULL);
ListHead->ForwardLink = ListHead;
ListHead->BackLink = ListHead;
return ListHead;
}
以上是分散在各个文件中的与头结点完整定义相关的语句,本质上gHandleList
在初始化的时候两个指针都是指向了自己。我们再来看插入IHANDLE
结构体的实现情况:
//MdePkg\Library\BaseLib\LinkedList.c
LIST_ENTRY *
EFIAPI
InsertTailList (
IN OUT LIST_ENTRY *ListHead,
IN OUT LIST_ENTRY *Entry
)
{
//
// ASSERT List not too long and Entry is not one of the nodes of List
//
ASSERT (InternalBaseLibIsNodeInList (ListHead, Entry, FALSE));
Entry->ForwardLink = ListHead;
Entry->BackLink = ListHead->BackLink;
Entry->BackLink->ForwardLink = Entry;
ListHead->BackLink = Entry;
return ListHead;
}
通过上述代码以及模拟情况我们可以很明确的得到结论,Handledatabase
链表是一个环形的链表,其中头结点是一个空节点gHandleList
,每次有新的IHANDLE
结构体需要添加到链表中,就在头结点后面进行插入。Handledatabase
就是我们需要强调掌握的第一个链表。
另外通过这个LIST_ENTRY
结构体的特点我们可以发现,出现这个结构体,必然表示存在一条对应的链表。我们观察可以发现在IHANDLE
结构体中,存在两个LIST_ENTRY
类型的成员,现在我们已经明确其中一条链表形成的是Handledatabase
,那么另外一条链表连接的是什么呢?这个问题就需要我们详细了解了protocol
之后回头在看了。
handle
以及其相关的的handledatabase
已经详细的解释过了,接下来我们再来看一下关于Protocol
的部分。
首先再次回顾EDK II UEFI Driver Writer’s Guide 中的表述如下:
The handle database is composed of objects called handles and protocols. Handles are a collection of one or more protocols and protocols are data structures named by a GUID. The data structure for a protocol may contain data fields, services, both or none at all.
protocol
是由GUID命名的一个数据结构。结构体中可能包含了data 或者service或者 两者都有 或者两者都没有。从这个定义中很难看出protocol
的实质,我们不妨继续从Code里面寻找答案
我们从上面知道protocol的唯一标识符就是GUID,那么在Code中,这个GUID是在什么位置如何表示出来的呢?看下面的定义
/// PROTOCOL_ENTRY - each different protocol has 1 entry in the protocol
/// database. Each handler that supports this protocol is listed, along
/// with a list of registered notifies.
///
typedef struct {
UINTN Signature;
/// Link Entry inserted to mProtocolDatabase
LIST_ENTRY AllEntries;
/// ID of the protocol
EFI_GUID ProtocolID;
/// All protocol interfaces
LIST_ENTRY Protocols;
/// Registerd notification handlers
LIST_ENTRY Notify;
} PROTOCOL_ENTRY;
上述结构体PROTOCOL_ENTRY
里面的成员ProtocolID
就是一个GUID,他就是protocol唯一的标识。由此可以确定我们所说的protocol一定是包含了PROTOCOL_ENTRY
这个结构体的。
看到这个结构体,一定会有似曾相识的感觉,熟悉的LIST_ENTRY
在这个结构体中出现了三次,根据之前我们的经验,那就说明,这个结构体的成员能够延展出三条双向链表。
首先看第一个AllEntries
,这个节点的无论是命名方式还是作用都和前面的AllHandle
是一样的,也就是PROTOCOL_ENTRY
结构体中的AllEntries
能够相互连接,形成一个环形的链表将PROTOCOL_ENTRY
以某种方式连接起来,我们将AllEntries
连接生成的链表称为ProtocolDatabase
.同样的,ProtocolDatabase
的头结点也是一个空节点ProtocolDatabase
// mProtocolDatabase - A list of all protocols in the system. LIST_ENTRY
mProtocolDatabase = INITIALIZE_LIST_HEAD_VARIABLE (mProtocolDatabase);
可以发现,与gHandleList
的定义方式完全一致,如果在代码中你进一步的深入挖掘,你就会发现,就连向ProtocolDatabase
插入新的PROTOCOL_ENTRY
的方式也是和向Handledatabase
插入新节点是一致的,这个部分可以参照函数CoreFindProtocolEntry
去分析,这个函数中最后
//
// Add it to protocol database
//
InsertTailList (&mProtocolDatabase, &ProtEntry->AllEntries);
就是插入新节点的对应操作。
ProtocolDatabase
就是我们需要强调掌握的第二个链表。
接下来我们观察第二条可以生成的链表 ,那就是以PROTOCOL_ENTRY
成员LIST_ENTRY Protocols
为头函数的另一个双向链表。在Code的注释中,能够清晰的看到这个成员连接了All protocol interfaces,那么我们接下来就看一看protocol interface 到底是一个什么样的结构?相关的各个链表是如何连接的?
protocol interface 的结构如下
///
/// PROTOCOL_INTERFACE - each protocol installed on a handle is tracked
/// with a protocol interface structure
///
typedef struct {
UINTN Signature;
/// Link on IHANDLE.Protocols
LIST_ENTRY Link;
/// Back pointer
IHANDLE *Handle;
/// Link on PROTOCOL_ENTRY.Protocols
LIST_ENTRY ByProtocol;
/// The protocol ID
PROTOCOL_ENTRY *Protocol;
/// The interface value
VOID *Interface;
/// OPEN_PROTOCOL_DATA list
LIST_ENTRY OpenList;
UINTN OpenListCount;
} PROTOCOL_INTERFACE;
在上面的结构体中我们看到熟悉的结构有:
三个LIST_ENTRY
成员表示存在三条双向链表
IHANDLE *Handle
指针和 PROTOCOL_ENTRY *Protocol
指针各一个
我们先说简单的部分 IHANDLE *Handle
和 PROTOCOL_ENTRY *Protocol
成员。以IHANDLE *Handle
成员为例,这是一个简单的单向指针,这个成员指针指向的就是自己所属的IHANDLE
结构体,也就是根据这个指针,我们能够轻松的找出当前protocol interface
所属的handle(),这就实现了根据已知的protocol interface
找到其所属的handle.
同理,PROTOCOL_ENTRY *Protocol
指向的是自己所属的PROTOCOL_ENTRY
结构体,这个指针说明了protocol interface 和protocol entry结构体之间是有着对应关系的,具体是什么样的对应关系后面我们详细讨论。其次这个指针也是一个单向的指针,表明我们能够通过该指针找出当前protocol interface
有联系的PROTOCOL_ENTRY
,这就实现了根据已知的protocol interface
找到其所属的PROTOCOL_ENTRY
.
在进行下面的讨论之前,我们还需要重点关注一下 VOID *Interface
这个成员,这个成员连接的是实际上有效地protocol实例,请注意,这里说的实例就是指我们能够自己定义、具有一定的功能的、别人能够调取的protocol。(仔细阅读LocateProtocol这个函数的定义,就会发现其实他也是Locate到该GUID对应的第一个Interface
,也可以证明实际上实现功能的是这个 VOID *Interface
)
到这里我们可以整理一下,当我们说安装protocol的时候,实际上是 生成一个新的PROTOCOL_INTERFACE
然后将我们定义好的具有实际功能的protocol挂在 VOID *Interface
下面,这样才算是完成了安装一个protocol的完整过程。
接下来我们看第二个成员LIST_ENTRY Link
,Code中的注释Link on IHANDLE.Protocols
告诉我们这个成员是与IHANDLE.Protocols
相连接形成链表的。那这样我们就能够得到一个以IHANDLE.Protocols
为头结点,PROTOCOL_INTERFACE.Link
为节点的双向链表。这也是在上一个部分中我们没有深入讲述的IHANDLE
的另一条双向链表。(可以回去看一下)
关于这个头结点的状况,明显在IHANDLE
结构体生成的时候就会对IHANDLE.Protocols
进行初始化
InitializeListHead (&Handle->Protocols);
LIST_ENTRY *
EFIAPI
InitializeListHead (
IN OUT LIST_ENTRY *ListHead
)
{
ASSERT (ListHead != NULL);
ListHead->ForwardLink = ListHead;
ListHead->BackLink = ListHead;
return ListHead;
}
可见头结点的初始情况与之前所说的头结点gHandleList
ProtocolDatabase
完全相同。
头结点讨论完,我们自然也想知道与之前讨论过的HandleDatabse链表 ProtocolDatabase链表对比,生成的这个链表是否仍然是环形的链表?新节点的插入与之前是否有什么不同?看下面一段代码,就能够找到答案。
//MdeModulePkg\Core\Dxe\Hand\Handle.c
/**
Installs a protocol interface into the boot services environment.
@param UserHandle The handle to install the protocol handler on,
or NULL if a new handle is to be allocated
@param Protocol The protocol to add to the handle
@param InterfaceType Indicates whether Interface is supplied in
native form.
@param Interface The interface for the protocol being added
@param Notify indicates whether notify the notification list
for this protocol
@retval EFI_INVALID_PARAMETER Invalid parameter
@retval EFI_OUT_OF_RESOURCES No enough buffer to allocate
@retval EFI_SUCCESS Protocol interface successfully installed
**/
EFI_STATUS
CoreInstallProtocolInterfaceNotify (
IN OUT EFI_HANDLE *UserHandle,
IN EFI_GUID *Protocol,
IN EFI_INTERFACE_TYPE InterfaceType,
IN VOID *Interface,
IN BOOLEAN Notify
)
{
//...
//
// Add this protocol interface to the head of the supported
// protocol list for this handle
//
InsertHeadList (&Handle->Protocols, &Prot->Link);
(上面一段代码对于我们理解整个handle & protocol 都很有帮助,后面也会反复的提到这个函数,当前只选取与这部分相关的代码进行说明)
上面的代码中,当我们知道了向Handle->Protocol
s为头结点的链表中插入新的节点Prot->Link
,调用的是一个与之前不同的函数InsertHeadList
(之前使用的是InsertTailList
),说明插入的方式还是有一定的区别的,那么看一下这个插入函数
LIST_ENTRY *
EFIAPI
InsertHeadList (
IN OUT LIST_ENTRY *ListHead,
IN OUT LIST_ENTRY *Entry
)
{
//
// ASSERT List not too long and Entry is not one of the nodes of List
//
ASSERT_VERIFY_NODE_IN_VALID_LIST (ListHead, Entry, FALSE);
Entry->ForwardLink = ListHead->ForwardLink;
Entry->BackLink = ListHead;
Entry->ForwardLink->BackLink = Entry;
ListHead->ForwardLink = Entry;
return ListHead;
}
我们模拟一下插入新节点的情况:
通过上述代码以及模拟情况我们可以得到结论,HANDLE-PROTOCOL-INTERFACE
链表是一个==环形的==链表,其中头结点是IHANDLE.Protocols
,每次有新的IHANDLE
结构体需要添加到链表中,就在最后一个节点后面进行插入。
HANDLE-PROTOCOL-INTERFACE
就是我们需要强调掌握的第三个链表。
前面我们已经说过IHANDLE *Handle
能够帮助我们单向的从protocol interface
找到其相关的 handle
,那么这个环形链表HANDLE-PROTOCOL-INTERFACE
的生成就能够帮助我们从Handle
出发,找到所有与之相联系的protocol interface
。这样 protocol interface
和Handle
之间的双向查找就都能够实现了。
接下来我们看第四个成员LIST_ENTRY ByProtocol
,Code中的注释/// Link on PROTOCOL_ENTRY.Protocols
清晰的说明了这个成员相关链表的情况,这个成员函数是和LIST_ENTRY
结构体的Protocols
成员相连接的。这样我们就能够得到一个以PROTOCOL_ENTRY.Protocols
为头结点PROTOCOL_INTERFACE.ByProtocol
为节点的双向链表。与之前讨论过HandleDatabse链表 ProtocolDatabase链表的链表对比,我们自然想知道,生成的这个链表是否仍然是环形的链表?新节点的插入与之前有什么不同?看下面一段代码,就能够找到答案
//MdeModulePkg\Core\Dxe\Hand\Handle.c
EFI_STATUS
CoreInstallProtocolInterfaceNotify (
IN OUT EFI_HANDLE *UserHandle,
IN EFI_GUID *Protocol,
IN EFI_INTERFACE_TYPE InterfaceType,
IN VOID *Interface,
IN BOOLEAN Notify
)
{
//...
// Add this protocol interface to the tail of the
// protocol entry
//
InsertTailList (&ProtEntry->Protocols, &Prot->ByProtocol);
//...
return Status;
上面的代码(上面的代码是不是很熟悉 如果你在EDKII中详细的观察就能够发现 基本上所有的相关的代码都能够在这部分函数中找到使用的情况)已经可以很清楚的展示出来,这个PROTOCOL-ENTRY-INTERFACE链表依然是一个环形的链表,每次新节点的插入情况和之前说过的 HandleDatabse链表 ProtocolDatabase链表是一致的。所以就不在赘述。
PROTOCOL-ENTRY-INTERFACE
就是我们需要强调掌握的第四个链表。
同理,protocol interface
和PROTOCOL_ENTRY
之间的双向查找也就能够根据PROTOCOL-ENTRY-INTERFACE
链表来实现了。
注意 我们在这里说明PROTOCOL-ENTRY-INTERFACE是一个链表,表明了一个PROTOCOL_ENTRY对应的可能是多个ROTOCOL_INTERFACE。前面我们说过 protocol的唯一标识符就是GUID,而GUID只存在于PROTOCOL_ENTRY中,所以我们可以说:一个Protocol实际上是由一个protocol entry 和 多个protocol interface构成的我个人认为这是一个重要的概念,这能够很好的帮助我们理解整个handle & protocol
结构。
接下来我们需要研究的就是 LIST_ENTRY OpenList
这个成员生成的链表了,(这个也是我目前不熟悉的链表,可能以后还需要专门研究一下这个链表)前面我们已经研究过四个链表的详细情况了,这个链表与前面几个链表的情况一模一样的。根据注释 LIST_ENTRY OpenList
连接的是OPEN_PROTOCOL_DATA
结构体
typedef struct {
UINTN Signature;
///Link on PROTOCOL_INTERFACE.OpenList
LIST_ENTRY Link;
EFI_HANDLE AgentHandle;
EFI_HANDLE ControllerHandle;
UINT32 Attributes;
UINT32 OpenCount;
} OPEN_PROTOCOL_DATA;
可以看到,这个结构体中唯一的一个能够形成双向链表的成员就是 LIST_ENTRY Link
,这个双向的链表就轻而易举的构成了。头结点PROTOCOL_INTERFACE.OpenList
,节点OPEN_PROTOCOL_DATA.Link
,采用的是InsertTailList
的插入方法。
OPEN_PROTOCOL_DATA
就是我们需要强调掌握的第五个链表。
下面来看PROTOCOL_ENTRY
结构体中能够延伸出的最后一个双向链表——成员LIST_ENTRY Notify
,这个成员形成的链表现在我还没有研究的十分透彻,就简单的说一下。这个成员连接的是PROTOCOL_NOTIFY
这个结构体,结构体的代码如下
///
/// PROTOCOL_NOTIFY - used for each register notification for a protocol
///
typedef struct {
UINTN Signature;
PROTOCOL_ENTRY *Protocol;
/// All notifications for this protocol
LIST_ENTRY Link;
/// Event to notify
EFI_EVENT Event;
/// Last position notified
LIST_ENTRY *Position;
} PROTOCOL_NOTIFY;
LIST_ENTRY Notify
与PROTOCOL_NOTIFY
的成员LIST_ENTRY Link
会形成一个双向的链表,这个链表最重要的作用就是PROTOCOL_NOTIFY
中的 EFI_GUID ProtocolID
对应的protocol安装时,该链表上所有的Event都会触发。(《UEFI编程与原理》)
根据之前的描述LIST_ENTRY *Position
应该也会形成一个双向的链表,但是这个链表的形成、连接、作用目前我还不知道,所以日后再说…
LIST_ENTRY Notify
这个链表介绍的很简单,主要还是因为我还没有理解十分透彻 挖坑后填吧…)
从上面的分析我们可以看出,handle&protocol
并不是孤立存在的,上述的六个链表相互交织,构成了庞大复杂的handle&protocol
表。将上面所有的链表详尽的画出来如下所示: