【UEFI】DXE阶段从概念到代码

总述

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 ServicesRuntime 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

这两个 ServiceOS Loader 提供了接口,用于访问硬件和软件资源。

同样地,UEFI 也分配了两个全局变量,gBSgRT来指代这两个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

实际上,ProtocolHandle中有很多双向链表,比较复杂,当然,不了解这一点也没问题。因为我们创建或者使用 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。

  1. 在目录edk2\OvmfPkg\Include\Protocol\新建一个文件HelloProtocol.h,用于定义 EFI_HELLO_PROTOCOL 的原型和功能函数的原型。内容如下:

    // edk2\OvmfPkg\Include\Protocol\HelloProtocol.h
    #ifndef __HELLO_PROTOCOL_H
    #define __HELLO_PROTOCOL_H

    EFI_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

  2. 在目录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()GuidProtocol 实例绑定。

另外,不要忘记,在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、查看运行结果,符合预期。


  1. UEFI学习笔记(十四):UEFI驱动的分类与UEFI驱动模型 ↩︎