总述
DXE(Driver Execution Environment)
阶段,是执行大部分系统初始化的阶段,也就是说是BIOS发挥作用,初始化整个主板的主战场。在这个阶段我们可以进行大量的驱动工作。
PEI 是 DXE 之前的阶段,负责初始化平台中的永久内存(相对于Cache
来说的内存,并非ROM),以便可以加载和执行 DXE 阶段。
PEI 阶段结束时的系统状态通过称为 Hand-Off Blocks (HOB)
的与位置无关的数据结构列表传递到 DXE 阶段。
There are several components in the DXE phase:
DXE 阶段有几个组件:
DXE Foundation
DXE 基础核心
DXE Dispatcher
DXE 调度器
A set of DXE Drivers
一组 DXE 驱动程序
从中可以看到,和 PEI 阶段的构成十分相似,这也印证了之前说的PEI 其实可以看作是 DXE 阶段的一个特殊微型版本。
DXE Foundation
生成一组 Boot Services
、Runtime Services
以及 DXE Services
.
DXE Dispatcher
负责按正确的顺序发现和执行 DXE Drivers
。
DXE Drivers
负责 初始化处理器、芯片组和平台组件,以及为系统服务、控制台设备和启动设备提供软件。
DXE 阶段和 引导设备选择 (BDS) 阶段协同工作,以建立控制台并尝试引导 OS。成功启动 OS 后,DXE 阶段将终止。
DXE Foundation
由启动服务代码组成,因此不允许将 DXE Foundation
本身的代码保留在 OS 运行时环境中。
仅允许 DXE Foundation
分配的运行时数据结构以及 驱动程序生成的服务和数据结构 保留在 OS 运行时环境中。
下面介绍一下DXE,本人初学者,一家之言,如有错误请留言指正。
DXE Foundation
DXE Foundation
,在代码中的实际表现为 DxeMain
函数,路径为edk2\MdeModulePkg\Core\Dxe\DxeMain\DxeMain.c
。
在DXE阶段,最重要的资源是 System Table
,如下 DxeMain.c
中初始化的 mEfiSystemTableTemplate
,这个变量将在随后被执行的代码中逐步完善此table。
EFI_SYSTEM_TABLE mEfiSystemTableTemplate = {
{
EFI_SYSTEM_TABLE_SIGNATURE, // Signature
EFI_SYSTEM_TABLE_REVISION, // Revision
sizeof (EFI_SYSTEM_TABLE), // HeaderSize
0, // CRC32
0 // Reserved
},
NULL, // FirmwareVendor
0, // FirmwareRevision
NULL, // ConsoleInHandle
NULL, // ConIn
NULL, // ConsoleOutHandle
NULL, // ConOut
NULL, // StandardErrorHandle
NULL, // StdErr
NULL, // RuntimeServices
&mBootServices, // BootServices
0, // NumberOfConfigurationTableEntries
NULL // ConfigurationTable
};
DXE 阶段提供的所有服务都可以通过 System Table
的指针进行访问。
这个变量如此重要,因而 UEFI 专门将这个 mEfiSystemTableTemplate
赋给一个全局变量gST
,方便我们调用。
具体的赋值逻辑如下:
DxeMain.c 中
gDxeCoreST = AllocateRuntimeCopyPool (sizeof (EFI_SYSTEM_TABLE), &mEfiSystemTableTemplate);
...
ProcessLibraryConstructorList (gDxeCoreImageHandle, gDxeCoreST);
...
||
||
\/
MdePkg\Library\UefiBootServicesTableLib\UefiBootServicesTableLib.c 中
....
gST = SystemTable;
....
在 System Table
中,有两个重要的 Service:BootServices
以及 RuntimeServices
。
这两个 Service
为 OS Loader
提供了接口,用于访问硬件和软件资源。
同样地,UEFI 也分配了两个全局变量,gBS
和gRT
来指代这两个Service。
在编写其他驱动或应用程序的时候,System Table
指针作为 Image
(就是其他的 UEFI 应用或 UEFI 驱动编译形成 .efi 文件被加载到内存后形成的东西) 的Entry Point
的参数传递进来,类似于 Main 函数的参数一样,因此,我们可以直接使用它。
每一个 .efi 文件加载到内存中,会变成Image
,UEFI 会创建ImageHandle
,我们可以用这个ImageHandle
来调用或做相关操作。
Image
的入口函数有统一的格式,可以在很多地方找到,可以查看 EDK2 中的例程学习。
例如,HelloWorld 应用:MdeModulePkg\Application\HelloWorld\HelloWorld.c
或者 I2C 驱动MdeModulePkg\Bus\I2c\I2cDxe\I2cDxe.c
,其函数原型如下:
typedef
EFI_STATUS
(EFIAPI *EFI_IMAGE_ENTRY_POINT)(
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
);
BootServices
首先介绍 SystemTable 中最重要的 BootServices
。
BootService
是 UEFI 的核心 API,可以做很多事情,例如内存分配释放、驱动管理、Protocol
的管理以及使用、UEFI 应用程序或驱动程序的加载、卸载、启动和退出等。
其中最重要的,也是我们接触最多的便是 Protocol
,其不仅是 DXE 阶段,也是我们整个 BIOS 的核心工作。
什么是Protocol
?
Protocol
UEFI 使用 Handle
来指代着我们需要初始化的诸多设备对象(例如PCIe设备),而 设备的驱动以 Protocol
的形式安装 到这个 Handle
上。
Protocol
的本质是一个结构体,这个结构体内存了很多的函数来实现不同的功能。其实Protocol
如同PEIM
中的PPI
,是一套功能的集合,里面就是一套函数集合+一个Guid。
Protocol
需要等到 DXE 阶段才可以使用(不需要特别在意 DXE 阶段的哪个点开始,基本上我们开发时写的 DXE 模块都可以使用)。
EDK2框架下,提供了现有的API函数来Install(安装)
、Open(打开)
、使用Protocol
等。
比如使用Protocol
内的功能前需要先打开Protocol ,有三个API可以打开,OpenProtocol()
、HandleProtocol()
和LocateProtocol()
.使用完毕要关闭Protocol
,使用CloseProtocol()
.
和PPI
一样,Protocol
必须先Install
才能使用。
Protocol
的作用跟普通的结构体没有区别,存放的是函数指针,可以调用来让特定功能代码执行。
UEFI下将大部分的设备初始化流程和其它功能都包装成了一个个的Protocol
,所以要学习UEFI,Protocol
是必经之路。
Protocol
在哪里?
具象到代码中,在MdeModulePkg\Core\Dxe\Hand\Handle.h
中定义了PROTOCOL_ENTRY
///
/// 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;
其他成员先不管,可以看到有一个EFI_GUID ProtocolID
以及LIST_ENTRY Protocols
,不难猜出,Protocol 被和Guid 以及其他一些信息一起封装,封装成为了PROTOCOL_ENTRY
。
实际上,Protocol
和Handle
中有很多双向链表,比较复杂,当然,不了解这一点也没问题。因为我们创建或者使用 Protocol 时使用 BootService(gBS 或者 gST->BootServices)的 API 函数来做。
回顾并总结一下。从具象的角度来说,Protocol是 一个个的结构体,包含了一些属性(成员变量)和函数指针(功能)。Protocol 是一个 DXE 驱动暴露给外界的服务,是提供者和使用者的一个约定,这个约定规范了提供服务或者使用服务所必须的一些流程和方式(例如要通过Guid来使用Protocol)。
代码
代码之前的一些概念
Protocol 可以被翻译为"服务",是用于向外界提供功能或者数据的接口。和驱动Driver的概念非常类似,但是在 UEFI 中,服务 Protocol 和驱动 Driver 是两个独立的概念,该如何理解?这涉及到了 UEFI 的驱动模型。
《UEFI 原理与编程》这本书中写:
服务与驱动不同,驱动需要特定的硬件的支持,而服务则不需要。
通常服务要能够常驻内存,而应用程序是不可常驻内存的,只有驱动可以。
所以,我们需要用驱动的形式来提供服务,这种被称作"服务型驱动"
在 UEFI 的标准中,驱动被分为两类:
一类是符合 UEFI 驱动模型的驱动,称为" UEFI 驱动";包括总线驱动、设备驱动和混合驱动。通过实现 Driver Binding Protocol 来控制设备。这些驱动程序可以动态地启动、停止和管理设备。
另一类是不遵循 UEFI 驱动模型的驱动,称为" DXE 驱动";有这些[[1]](#[1]):
(1)服务型驱动 (Protocol)
不管理任何设备,不需要硬件支持,用来产生protocol提供功能服务。
一般来说,服务是可以常驻内存的,应用程序不能常驻内存,只有驱动可以,所以用驱动的形式来提供服务,称之为服务型驱动。
(2)初始化驱动
不产生任何句柄,用来做一些初始化操作,执行完后就会从系统中卸载。
(3)根桥型驱动
用来初始化平台上的根桥控制器,并产生一个设备地址 Protocol,以及访问总线设备的 Protocol。
一般用来通过总线驱动访问设备。比如,使用的支持访问 PCIe/PCI 设备的 EFI_PCI_ROOT_BRIDGE_IO_PROTOCOL
UEFI Driver 主要用于管理 PCI 设备,采用分层架构,具有良好的模块化特性,层次结构清晰。
相较之下,DXE Driver 主要负责平台的初始化工作以及一些功能服务。
有一个感性认识:
UEFI 驱动
的执行流程为:
-> UEFI 驱动被加载到内存中
-> EntryPoint 入口函数
-> 执行 gBS->InstallProtocolInterface()
-> 通过 Driver Binding Protocol (struct EFI_DRIVER_BINDING_PROTOCOL) 以及 Component Name Protocol 这两个服务,安装驱动到自身的 Handle 或其他 Handle 上
-> 使用 Driver Binding Protocol 给的三个API来管理 驱动以及其 Protocol
而服务型驱动
则很简单,具体流程为:
-> EntryPoint 入口函数
-> 将Protocol安装到自身的Handle
"服务型驱动"并不遵循 UEFI 驱动模型,因此是属于 DXE 驱动。
有以下几个特点:
- 在 Image 的入口函数中执行安装,因此也无法进行多次安装(无法卸载再安装,必须卸载整个驱动文件重新执行 loadImage 命令,即再次进入驱动文件的入口函数)
- 不需要驱动特定的硬件,可以单纯的是软件功能,所以可以安装到任意的控制器(设备)上
- 没有提供卸载函数
所以服务型驱动(DXE驱动),可以看作是一种简易版本的 UEFI 驱动。
因此,下面以 DXE 驱动为例子,进行代码实践。
Install一个自己的Protocol
与 PEI 阶段中的 PPI 类似,Protocol 在使用之前也需要安装。
与 PPI 不同的是,Protocol 需要安装在 Image 对象的句柄(Handle)上。
BootService 提供了一个 API,InstallProtocolInterface,可以通过gBS->InstallProtocolInterface 来安装 Protocol。其定义如下:MdePkg\Include\Uefi\UefiSpec.h
/**
Installs a protocol interface on a device handle. If the handle does not exist, it is created and added
to the list of handles in the system. InstallMultipleProtocolInterfaces() performs
more error checking than InstallProtocolInterface(), so it is recommended that
InstallMultipleProtocolInterfaces() be used in place of
InstallProtocolInterface()
@param[in, out] Handle A pointer to the EFI_HANDLE on which the interface is to be installed.
@param[in] Protocol The numeric ID of the protocol interface.
@param[in] InterfaceType Indicates whether Interface is supplied in native form.
@param[in] Interface A pointer to the protocol interface.
@retval EFI_SUCCESS The protocol interface was installed.
@retval EFI_OUT_OF_RESOURCES Space for a new handle could not be allocated.
@retval EFI_INVALID_PARAMETER Handle is NULL.
@retval EFI_INVALID_PARAMETER Protocol is NULL.
@retval EFI_INVALID_PARAMETER InterfaceType is not EFI_NATIVE_INTERFACE.
@retval EFI_INVALID_PARAMETER Protocol is already installed on the handle specified by Handle.
**/
typedef
EFI_STATUS
(EFIAPI *EFI_INSTALL_PROTOCOL_INTERFACE)(
IN OUT EFI_HANDLE *Handle,
IN EFI_GUID *Protocol,
IN EFI_INTERFACE_TYPE InterfaceType,
IN VOID *Interface
);
步骤
Protocol
是一套功能函数和数据的集合,所以 Protocol
是一个结构体。
我们需要自己定义这个结构体的原型,然后实例化这个结构体。
紧接着,再将这个 Protocol
实例和一个 Guid
绑定,即 完成安装。
我们以一个打印 Hello Protocol
字符串的例子,来 Install
一个 名为EFI_HELLO_PROTOCOL
的 Protocol。
-
在目录
edk2\OvmfPkg\Include\Protocol\
新建一个文件HelloProtocol.h
,用于定义EFI_HELLO_PROTOCOL
的原型和功能函数的原型。内容如下:// edk2\OvmfPkg\Include\Protocol\HelloProtocol.h
#ifndef __HELLO_PROTOCOL_H
#define __HELLO_PROTOCOL_HEFI_GUID gEfiHelloProtocolGuid= {0x2b35952b, 0xa6dc, 0x4181, {0xa2, 0xab, 0x95, 0x89, 0xbe, 0xcf, 0x4c, 0xb3}};
typedef struct _EFI_HELLO_PROTOCOL EFI_HELLO_PROTOCOL;
// Protocol功能函数的定义
typedef
EFI_STATUS
(EFIAPI *PRINT_HELLO)(
IN EFI_HELLO_PROTOCOL *This
// 按照 UEFI 驱动模型,第一个参数需要是指向
// 这个函数所属的 Protocol的This指针,虽然我们是
// DXE 驱动,所撰写的 Protocol 也并无意和任何硬件绑定
// 但是我们为保证一致性仍然遵循这个规范
);// HelloProtocol结构体定义
struct _EFI_HELLO_PROTOCOL{
UINTN Data;
PRINT_HELLO Hello;
};#endif // !__HELLO_PROTOCOL_H
-
在目录
edk2\OvmfPkg\
下新建目录MyHelloProtocolInstall :即edk2\OvmfPkg\MyHelloProtocolInstall
,并创建两个文件
MyHelloProtocolInstall.c
以及MyHelloProtocolInstall.inf
// MyHelloProtocolInstall.c 文件内容
#include <Uefi.h>#include <Library/UefiDriverEntryPoint.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/DebugLib.h>#include <Protocol/HelloProtocol.h>
// 1、实现Protocol的功能函数
EFI_STATUS
EFIAPI
PrintHello(
IN EFI_HELLO_PROTOCOL *This
)
{
DEBUG((EFI_D_ERROR, "[MyHelloProtocol] Hello Protocol!\r\n"));return EFI_SUCCESS;
}// 入口函数
EFI_STATUS
EFIAPI
ProtocolServerEntry (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
EFI_HELLO_PROTOCOL *Protocol;Status = EFI_SUCCESS;
// 2、实例化Protocol,分配相应的内存空间
Protocol = AllocatePool(sizeof(EFI_HELLO_PROTOCOL));
if (NULL == Protocol)
{
DEBUG((EFI_D_ERROR, "[MyHelloProtocol] Protocol Memory Allocate Failed!\r\n"));
return EFI_OUT_OF_RESOURCES;
}
// 为Protocol的成员赋值
Protocol->Data = 0x01;
Protocol->Hello = PrintHello;// 3、Install 这个 Protocol
Status = gBS->InstallProtocolInterface(
&ImageHandle,
&gEfiHelloProtocolGuid,
EFI_NATIVE_INTERFACE,
Protocol
);
// 安装失败的处理
if (EFI_ERROR (Status)) {
DEBUG ((EFI_D_ERROR, "[MyHelloProtocol] Install EFI_HELLO_PROTOCOL Failed! Code - %r\n", Status));
FreePool (Protocol);
Protocol = NULL;
return Status;
}return Status;
}// MyHelloProtocolInstall.inf 文件内容
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = MyHelloProtocolInstall
FILE_GUID = b885710c-40f9-4a92-a5ce-022829746c5e
MODULE_TYPE = UEFI_DRIVER
VERSION_STRING = 1.0
ENTRY_POINT = ProtocolServerEntry[Sources.common]
MyHelloProtocolInstall.c[Packages]
MdePkg/MdePkg.dec
OvmfPkg/OvmfPkg.dec # 如果不包含自己的这个包,那么头文件就
# 需要写为 #include "../Include/Protocol/HelloProtocol.h"[LibraryClasses]
UefiDriverEntryPoint
UefiBootServicesTableLib
MemoryAllocationLib
DebugLib[Protocols]
[Depex]
TRUE
安装一个 Protocol
就是实现一个 Protocol
,因此需要
1、 实例化 Protocol
结构体(在这之前需要实现 Protocol
内的函数)
2、 调用 gBS->InstallProtocolInterface()
将 Guid
和 Protocol
实例绑定。
另外,不要忘记,在OvmfPkg\OvmfPkgX64.dsc
的[Components]
中添加我们的.inf
文件,这样才会被编译。
以及 OvmfPkg\OvmfPkgX64.fdf
中的[FV.DXEFV]
,增加如下:
使用Protocol
使用 Protocol
的方式有很多,主要是 BS 中的 OpenProtocol()
、HandleProtocol()
以及 LocateProtocol()
函数。
gBS->OpenProtocol()
和 gBS->HandleProtocol()
的功能主要是打开指定设备(入参 Handle)中安装的某个Protocol
。
由于我们期望调用的我们自己的 HelloProtocol
是属于服务型 Protocol
,因此我们并不关心这个 Protocol
具体在哪个设备上。
另外,系统中仅仅只有一个我们的 HelloProtocol
的实例,所以,我们使用 gBS->LocateProtocol()
来找到我们安装好的 HelloProtocol
。
回顾一下,在 DXE 阶段中,Protocol 是 被DXE Foundation 自动调度到我们的 MyHelloProtocolInstall 后,进行安装的。
如果要使用这个 Protocol,可以写一个名为MyHelloProtocolLocate
的应用程序,即类型为UEFI Application来调用。
具体步骤
1、在目录OvmfPkg\MyHelloProtocolLocate\
下分别创建MyHelloProtocolLocate.c
以及MyHelloProtocolLocate.inf
2、编写这两文件,内容如下:
# MyHelloProtocolLocate.inf
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = MyHelloProtocolLocate
FILE_GUID = 554b3cbf-af08-44c7-829f-13a59ee0bf21
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 1.0
ENTRY_POINT = ProtocolConsumerEntry
[Sources]
MyHelloProtocolLocate.c
[Packages]
MdePkg/MdePkg.dec
OvmfPkg/OvmfPkg.dec # 如果不包含这个包,那么头文件就需要写为 #include "../Include/Protocol/HelloProtocol.h"
[LibraryClasses]
UefiApplicationEntryPoint
UefiBootServicesTableLib
MemoryAllocationLib
DebugLib
UefiLib
// MyHelloProtocolLocate.c
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/BaseLib.h>
#include <Library/DebugLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include "Protocol/HelloProtocol.h"
EFI_STATUS
EFIAPI
ProtocolConsumerEntry(
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
EFI_HELLO_PROTOCOL *Protocol;
Status = EFI_SUCCESS;
DEBUG ((EFI_D_ERROR , "[MyHelloProtocol] MyHelloProtocol App ProtocolEntry Start..\n"));
Print (L"[MyHelloProtocol] MyHelloProtocol App ProtocolConsumerEntry Has Started..\n");
// 1、根据Guid, Locate Protocol,LocateProtocol()会自动将其装填进 第三个参数 Protocol这个变量里
Status = gBS->LocateProtocol(
&gEfiHelloProtocolGuid,
NULL,
(VOID **)&Protocol
);
// locate失败的操作
if (EFI_ERROR (Status)) {
DEBUG ((EFI_D_ERROR, "[MyHelloProtocol] Locate EFI_HELLO_PROTOCOL Failed! - %r\n", Status));
Print(L"[MyHelloProtocol] Locate Protocol gEfiHelloProtocolGuid Failed - Code: %r \n",Status);
return Status;
}
// 2、使用 Protocol
// 拿Protocol内的数据
DEBUG ((EFI_D_ERROR, "[MyHelloProtocol] Protocol Version: 0x%08x\n", Protocol->Data));
// 调Protocol内的功能 ---- Hello
Status = Protocol->Hello (Protocol);
if (EFI_ERROR (Status)) {
DEBUG ((EFI_D_ERROR, "[MyHelloProtocol] Protocol->Hello Failed! - %r\n", Status));
return Status;
}
DEBUG ((EFI_D_ERROR, "[MyHelloProtocol] MyHelloProtocol End..\n"));
Print (L"[MyHelloProtocol] MyHelloProtocol End ... \n");
return Status;
}
.c
文件大概的逻辑如下:
- 根据 guid,找到 MyHelloProtocol,并将其装填进名为
Protocol
的局部变量中。 - 使用 Protocol,根据函数指针调用功能或者直接拿取数据。
3、在edk目录下,先执行 ./edksetup.bat
,再 编译
build -a X64 -p OvmfPkg\OvmfPkgX64.dsc -D DEBUG_ON_SERIAL_PORT
4、在edk同级目录下创建ovmf文件夹
再创建 D:\edk2\ovmf\esp
文件夹,并且将D:\edk2\edk2\Build\OvmfX64\DEBUG_VS2019\X64\MyHelloProtocolLocate.efi
复制到上面的目录里D:\edk2\ovmf\esp\MyHelloProtocolLocate.efi
。
这一步是为了创建了一个分区,等会进 UEFI Shell中,挂载磁盘,方便我们执行UEFI App
5、进入qemu的文件夹,并且进入终端执行qemu-system-x86_64.exe -bios D:\edk2\edk2\Build\OvmfX64\DEBUG_VS2019\FV\OVMF.fd -hda fat:rw:D:\edk2\ovmf\esp -net none -serial stdio | findstr MyHelloProtocol
如图:
6、运行App
进入shell后,输入fs0:
,在ls
命令查看文件,找到我们的MyHelloProtocolLocate.efi
并执行。
7、查看运行结果,符合预期。