【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 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)服務型驅動 (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_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
- 在目錄
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、查看運行結果,符合預期。


浙公網安備 33010602011771號