【UEFI】PEI階段從概念到代碼
總述
UEFI開發過程中,BIOS工程師主要關注點和工作都在于PEI(Pre-EFI Initialization,EFI前初始化)和DXE(Driver Execution Environment)階段。
DXE階段是我們的主戰場,可以進行豐富且大量的功能驅動開發。
一陣見血。
我們換句話說,PEI階段是進入DXE階段前的一個不得已而為之的妥協,或是一個過渡的階段,我們的目標是進入DXE階段,能夠放開拳腳。
下面介紹一下PEI,本人初學者,一家之言,如有錯誤請留言指正。
為什么有PEI階段
在PEI階段在SEC階段之后,盡管進行了SEC的相關工作,但仍然相對初始。
尤其是內存仍然尚未初始化,而想要利用C語言來做一些豐富的功能開發,盡快進入DXE階段,最關鍵的是能夠大量地使用“棧”。
因此在這個階段,我們希望可以盡快能夠初始化Memory,在一些資料中也被稱為“永久內存Permanent Memory”。
此處的永久內存仍然是指Ram,即斷電易失的存儲器,永久是相對于SEC階段中的Cache As Ram (CAR)來說的。
在這個階段僅利用 CPU 上的資源,如將 CPU 的緩存 Cache 作為棧,來調度PEIM(PEI Module),目的是最快進入DXE階段。這些 PEIM 負責以下工作:
UEFI PI Spec 1.8中這樣描述的:
Initializing some permanent memory complement
初始化一些 永久性內存 作為補充
Describing the memory in Hand-Off Blocks (HOBs)
描述 傳遞塊(HOBs)中的內存
Describing the firmware volume locations in HOBs
描述 HOBs 中的固件卷位置
Passing control into the Driver Execution Environment (DXE) phase
將控制權傳遞到 驅動執行環境(DXE)階段
Philosophically, the PEI phase is intended to be the thinnest amount of code to achieve the ends listed above. As such, any more sophisticated algorithms or processing should be deferred to the DXE phase of execution.
從哲學上講,PEI 階段應該以最少的代碼量實現上述目標。因此,任何更復雜的算法或處理都應該推遲到執行 DXE 階段。
...............
名詞很多,而且真的很抽象。
那首先,PEIM是什么?
PEIM
PEIM,PEI階段對系統的初始化主要由PEIM完成。
在具體地認知上,可以認為是一個個的 *.efi 二進制文件。
可以認為,這些個efi文件就是在UEFI下的可執行文件,類似于我們在單片機中燒寫的二進制.bin文件。
- 資料中說,
.efi文件格式是基于PE32+的文件格式而來,具體這個PE32+格式是個啥,我們先不細究,反正也細究不明白。
更具象地,在編譯后的Build文件夾中,例如在 \edk2\Build\OvmfX64\DEBUG_VS2019\X64\這個文件夾下,可以找到大量的 .efi 文件,其中有一部分形如 XxxxxxPei.efi 的文件,例如 S3Resume2Pei.efi文件,使用WinHex等軟件可以打開,查看其格式。
流程是:.inf 文件 + .c 文件 + .h 文件 -> build -> .efi
知道了什么是PEIM了,那PEIM這些功能模塊是怎么怎么在代碼中跑起來的呢?下面我們來看下。
一些概念
-
PEI 內核(在UEFI Spec中叫
PEI Foundation,在EDK2代碼中其實就是PeiCore):負責PEI階段的基礎服務和流程,可以認為是PEI階段的內核,在EDK2代碼中,具體可以找到MdeModulePkg\Core\Pei\PeiMain\PeiMain.c中的函數PeiCore
![image]()
-
PEIM Dispatcher(調度器):具體地是在PeiCore中PeiDispatcher函數,Dispatcher會找出系統中的所有PEIM,并根據PEIM之間的依賴關系,按順序執行PEIM。
![image]()
-
PEI Foundation,即PeiCore,會建立一個 UEFI規范里叫 PEI Services Table 的變量,實際在代碼里如下圖中的gPs,該表對所有系統中的 PEIM 可見。通過PEI Services,PEIM 可以調用 PEI 階段提供的一些系統功能,例如
Install PPI、Locate PPI以及Notify PPI等。
![image]()
(另外說一嘴,在EDK2中,如果是全局變量就用gVariable的小駝峰形式來標注,如果是僅僅在Module中使用的變量,則mVariable來命名) -
通過調用這些服務,PEIM可以訪問PEI內核。PEIM之間的的通信通過PPI(PEIM-to-PEIM Interfaces)完成。
啥又是Interface?
PPI(PEIM-to-PEIM Interfaces)
在EDK2中,Interface接口的概念使用非常多,然而這里的接口并不是類似于Java或者Web的前后端通信的接口。具體在代碼的表現上,其實就是一個結構體,這個結構體描述了某一個函數功能的信息,相當于把一個功能函數封裝起來。
在MdePkg\Include\Pi\PiPeiCis.h中可以看到

PPI 是用 EFI_PEI_PPI_DESCRIPTOR 來封裝描述的,里面有個成員是 VOID *Ppi。
這個成員是個指針,一旦初始化這個描述符,也就是說我們綁定了 某個 Guid 和 某個 Ppi 上,并且通過Flags來指定這個Ppi的一些屬性。不要忘了,PPI本質上是希望給其他PEIM調用的功能,所以具體的功能函數就應該存放在這個VOID *Ppi里。
前面我們也說了,接口本身是一個結構體,這個VOID *Ppi所以也應該是一個結構體。不信?我們看EDK2中的代碼,看看大佬的寫法:

可以從上圖中看到,首先定義了一個Const EFI_XXX_XXX_PPI類型的 mXxxxPpi,因此,可以說,PPI是一個結構體。這個例子中,結構體中只有一個成員WaitForNotify,這個成員是一個函數。
在實際開發中,Const EFI_XXX_XXX_PPI類型應當是由我們自己定義的, 為啥呢?
想想開發PEIM的流程,我們應當預先寫好相關的函數功能,例如Func1、Func2、Func3,再將這些Func1、Func2、Func3統統包含到一個結構體里,那如何把函數包含到結構體里?當然是自己定義結構體原型了。例如:
// 函數原型,注意這里的函數是沒有函數體的
typedef
EFI_STATUS
(EFIAPI *EFI_PEI_FUNC_1)();
typedef
EFI_STATUS
(EFIAPI *EFI_PEI_FUNC_2)();
typedef
EFI_STATUS
(EFIAPI *EFI_PEI_FUNC_3)();
// PPI結構體原型定義
typedef struct _EFI_PEI_FUNC1_FUNC2_FUNC3_PPI
{
EFI_PEI_FUNC_1 func1;
EFI_PEI_FUNC_2 func2;
EFI_PEI_FUNC_3 func3;
} EFI_PEI_FUNC1_FUNC2_FUNC3_PPI;
// 函數功能實現
EFI_STATUS
EFIAPI
Func1(){
.......
return EFI_SUCCESS;
}
EFI_STATUS
EFIAPI
Func2(){
.......
return EFI_SUCCESS;
}
EFI_STATUS
EFIAPI
Func3(){
.......
return EFI_SUCCESS;
}
// 重點來了,實例化Ppi結構體
EFI_PEI_FUNC1_FUNC2_FUNC3_PPI mFunc1Func2Func3Ppi = {
Func1,
Func2,
Func3
};
緊接著,又利用 EFI_PEI_PPI_DESCRIPTOR 這個描述符封裝這個結構體,并指定其Flags屬性和綁定Guid,這樣以后我們就可以通過Guid來找到這個PPI,從而調用到PPI里的功能了,是不是很麻煩聰明?
EFI_PEI_PPI_DESCRIPTOR mFunc1Func2Func3PpiList = {
(EFI_PEI_PPI_DESCRIPTOR_PPI | EFI_PEI_PPI_DESCRIPTOR_TERMINATE_LIST),
&gEfiFunc1Func2Func3PpiGuid, // 這個GUID在開頭自己定義好,或者使用一些UEFI中的,可以實現一些功能
&mFunc1Func2Func3Ppi
};
現在我們知道了怎么定義一個PPI,那該如何完整的開發一個PPI或使用一個PPI呢?
動手實踐
Install 一個自己的 PPI
這里就涉及到了如何編寫一個PEIM模塊了,實際上上面的定義一個PPI內容都是某一個xxxPEIM.c的內容。
新建一個文件夾(就是PEIM),路徑為edk2\OvmfPkg\MyHelloWorldInstallPpi\,創建兩個文件,分別叫做MyHelloWorldInstallPpi.c 、 MyHelloWorldInstallPpi.inf

MyHelloWorldInstallPpi.inf
[Defines]
INF_VERSION = 0x00010005
VERSION_STRING = 1.0
BASE_NAME = MyHelloWorldInstallPpi
MODULE_TYPE = PEIM # 這里必須得是PEIM,表明我們要創建的是一個PEI Module
FILE_GUID = c4f822d4-02e0-4ebf-854d-390dc8ca6166
ENTRY_POINT = MyInstallPpiEntryPoint # 入口函數可以自己隨便起名字,只要和.c文件中的一致即可
[Sources]
MyHelloWorldInstallPpi.c
# 我們這一次實驗只有這一個.c函數,我們創建自己的PPI,
# 功能是輸出HelloWorld的debug信息,并且將其Install到PPI Database中,
# 方便后續我們自己調用
[LibraryClasses]
BaseLib
PeimEntryPoint
BaseMemoryLib
DebugLib
PeiServicesLib
PrintLib
[Packages]
MdePkg/MdePkg.dec
ShellPkg/ShellPkg.dec
MdeModulePkg/MdeModulePkg.dec
[Pcd]
[Ppis]
[Depex]
TRUE
MyHelloWorldInstallPpi.c
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/BaseLib.h>
#include <Library/IoLib.h>
#include <Library/DebugLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/PeimEntryPoint.h>
#include <Library/PeiServicesLib.h>
#include <Library/PeiServicesTablePointerLib.h>
#include <Pi/PiHob.h>
#include <Pi/PiPeiCis.h>
EFI_GUID gEfiHelloWorldPpiInstallGuid = {0xf0915e25, 0xe749, 0x4a7a, {0x9f, 0x31, 0xbd, 0xb5, 0x4c, 0x05, 0x22, 0xc4}};
/********************************************************************************
* 當需要將一個PEIM的代碼共享給其它PEIM調用的時候,就可以把它安裝在PPI的數據庫 PPI Database中。
*
* 步驟:
* 1、定義PPI結構體并實例化,結構體里面是具體的功能函數(函數指針)實現
*
* 2、將PPI結構體添加到EFI_PEI_PPI_DESCRIPTOR PPI_List[],這個數組里都是PPI函數指針的struct
*
* 3、在入口函數中Install PPI_List[],將這一套PPI注冊在Database中。
*
********************************************************************************/
// 定義PPI功能函數接口原型和結構體
typedef
EFI_STATUS
(EFIAPI *EFI_PRINT_HELLO_WORLD_MSG)(
IN CHAR16 *Msg
);
typedef struct _EFI_PEI_PRINT_HELLO_WORLD_MSG_PPI
{
EFI_PRINT_HELLO_WORLD_MSG peiPrintHelloWorldMsg;
} EFI_PEI_PRINT_HELLO_WORLD_MSG_PPI;
// 實現PPI函數功能,并緊接著實例化結構體
// 功能:打印任意字符串Msg
EFI_STATUS
EFIAPI
PrintHelloMsg (
IN CHAR16 *Msg
)
{
DEBUG ((EFI_D_ERROR, "[MyHelloWorldInstallPpi] PRINT_HELLO_WORLD_MSG is called \r\n"));
DEBUG ((EFI_D_ERROR, "[MyHelloWorldInstallPpi] PrintHelloMsg : %s \r\n", Msg));
return EFI_SUCCESS;
}
// 實例化PPI結構體
EFI_PEI_PRINT_HELLO_WORLD_MSG_PPI mPeiHelloPpi = {
PrintHelloMsg
};
// 添加進PPI_LIST[],并且將PPI和相關的guid綁定
EFI_PEI_PPI_DESCRIPTOR mPeiHelloPpiList[] = {
{
(EFI_PEI_PPI_DESCRIPTOR_PPI | EFI_PEI_PPI_DESCRIPTOR_TERMINATE_LIST),
&gEfiHelloWorldPpiInstallGuid,
&mPeiHelloPpi
}
};
/*
* @brief PEIM 的入口函數,PEIM的main函數
*
* @return 狀態碼
*/
EFI_STATUS
EFIAPI
MyInstallPpiEntryPoint(
IN EFI_PEI_FILE_HANDLE FileHandle,
IN CONST EFI_PEI_SERVICES ** PeiServices
)
{
EFI_STATUS status;
DEBUG ((EFI_D_ERROR, "[MyInstallPpiEntryPoint] MyInstallPpiEntryPoint Start..\r\n"));
// Install PPI
status = (*PeiServices) ->InstallPpi (PeiServices, &mPeiHelloPpiList[0]);
// Install 失敗的處理
if (EFI_ERROR(status))
{
DEBUG ((EFI_D_ERROR, "[MyInstallPpiEntryPoint] Install PPI failed.. \r\n"));
DEBUG ((EFI_D_ERROR, "[MyInstallPpiEntryPoint] EFI return value is %d \r\n", status));
return status;
}
// Install 成功,打印通知
DEBUG ((EFI_D_ERROR, "[MyInstallPpiEntryPoint] Install PPI success! \r\n"));
DEBUG ((EFI_D_ERROR, "[MyInstallPpiEntryPoint] MyHelloWorldInstallPPIEntry End.. \r\n"));
return EFI_SUCCESS;
}
這樣,就成功的開發了一個PPI。
這個PPI會在PeiCore中受到PeiDispatchor調度,自動運行。
但是我們還不能直接用這個PPI。
上面說過,PPI是PEIM之間的通信方式。
也就是說,PPI是PEIM的對外暴露給其他PEIM的功能接口,因此,我們Install好了PPI還需要再寫一個PEIM,來使用我們現在寫好的這個PPI。
Locate 一個自己的 PPI
Locate PPI,如同Install PPI,也就是PEI Services里,gPs里,EDK2已經給我們寫好的一個API.
新建一個文件夾(就是PEIM),路徑為edk2\OvmfPkg\MyHelloWorldLocatePpi\,創建兩個文件,分別叫做MyHelloWorldLocatePpi.c 、 MyHelloWorldLocatePpi.inf

MyHelloWorldLocatePpi.inf
[Defines]
INF_VERSION = 0x00010005
VERSION_STRING = 1.0
BASE_NAME = MyHelloWorldLocatePpi
MODULE_TYPE = PEIM
FILE_GUID = af521e0f-4aef-498a-8f19-b1de83a77c70
ENTRY_POINT = MyLocatePpiEntryPoint
[Sources]
MyHelloWorldLocatePpi.c
[LibraryClasses]
BaseLib
PeimEntryPoint
BaseMemoryLib
DebugLib
PeiServicesLib
PrintLib
[Packages]
MdePkg/MdePkg.dec
ShellPkg/ShellPkg.dec
MdeModulePkg/MdeModulePkg.dec
OvmfPkg/OvmfPkg.dec # 多一個我們寫PPI的那個Pkg
[Pcd]
[Ppis]
gEfiHelloWorldPpiInstallGuid
# 用到了Install這個PEM的PPI,所以要告訴本模塊,
# 該PPI的guid,用于查找;
# 另外,也可以在C文件中直接調用,更方便
[Depex]
gEfiHelloWorldPpiInstallGuid
# 這邊是使用我們自己創建的PpiGuid的,
# 這樣可以確保我們的調用Ppi的函數時,
# 該Ppi已經被Install了。
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/BaseLib.h>
#include <Library/IoLib.h>
#include <Library/DebugLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/UefiDriverEntryPoint.h>
#include <Library/PeimEntryPoint.h>
#include <Library/PeiServicesLib.h>
#include <Library/PeiServicesTablePointerLib.h>
#include <Pi/PiHob.h>
#include <Pi/PiPeiCis.h>
// EFI_GUID gEfiHelloWorldPpiInstallGuid = {0xf0915e25, 0xe749, 0x4a7a, {0x9f, 0x31, 0xbd, 0xb5, 0x4c, 0x05, 0x22, 0xc4}};
// 定義PPI功能函數接口原型和結構體
typedef
EFI_STATUS
(EFIAPI *EFI_PRINT_HELLO_WORLD_MSG)(
IN CHAR16 *Msg
);
typedef struct _EFI_PEI_PRINT_HELLO_WORLD_MSG_PPI
{
EFI_PRINT_HELLO_WORLD_MSG peiPrintHelloWorldMsg;
} EFI_PEI_PRINT_HELLO_WORLD_MSG_PPI;
EFI_STATUS
EFIAPI
MyLocatePpiEntryPoint(
IN EFI_PEI_FILE_HANDLE FileHandle,
IN CONST EFI_PEI_SERVICES ** PeiServices
)
{
EFI_STATUS Status;
// 定義一個變量,用于接收解析到的PPI,相當于接受實例
EFI_PEI_PRINT_HELLO_WORLD_MSG_PPI *mHelloWorldPpi = NULL;
DEBUG ((EFI_D_ERROR, "[MyLocatePpiEntryPoint] MyLocatePpiEntryPoint Locate PPI Start..\n"));
// Locate PPI
Status = PeiServicesLocatePpi (
&gEfiHelloWorldPpiInstallGuid,// 這里的GUID雖然沒有定義也沒有extern,但是因為我們在inf里寫了,所以可以直接用
0,
NULL,
(VOID **)&mHelloWorldPpi
);
if (EFI_ERROR(Status))
{
DEBUG ((EFI_D_ERROR, "[MyLocatePpiEntryPoint] Locate PPI failed..\r\n"));
DEBUG ((EFI_D_ERROR, "[MyInstallPpiEntryPoint] EFI return value is %d \r\n", Status));
return Status;
}
// Locate 成功,打印通知
DEBUG ((EFI_D_ERROR, "[MyLocatePpiEntryPoint] Locate PPI success! \r\n"));
// 調用PPI內的功能
mHelloWorldPpi-> peiPrintHelloWorldMsg(L"2025 Tyler Wang Locate PPI Hello World ...\n");
DEBUG ((EFI_D_ERROR, "[MyLocatePpiEntryPoint] MyLocatePpiEntryPoint Locate PPI End..\n"));
return EFI_SUCCESS;
}


編譯
進入edk2目錄,在edksetup.bat最后一行添加
build -a X64 -p OvmfPkg\OvmfPkgX64.dsc -D DEBUG_ON_SERIAL_PORT
這樣以后打開cmd之后,只需要運行edksetup.bat即可自動編譯出.fd文件。
編譯通過之后,使用qemu模擬器。
在qemu模擬器的路徑下,例如我是D:\Program Files\qemu,創建setup-qemu-x64.bat文件。
里面內容是:
"D:\Program Files\qemu\qemu-system-x86_64.exe" -bios "D:\edk2\edk2\Build\OvmfX64\DEBUG_VS2019\FV\OVMF.fd" -M "pc" -m 256 -cpu "qemu64" -boot order=dc -serial stdio
這里面的路徑請根據自己打情況自行修改。
在qemu模擬器的路徑下,cmd運行setup-qemu-x64.bat | findstr "Hello World",如下圖

可以觀察到Hello World現象了。
后記
InstallPpi.c文件寫好了之后,我中間編譯了好幾次,一直顯示fail,如下圖:

一直以為是我的cl.exe環境配置有問題
NMAKE : fatal error U1077: D:\Develop\Microsoft\VisualStudio\2019\Community\VC\Tools\MSVC\14.29.30133\bin\Hostx86\x64\cl.exe: ??0x2
Stop.
然而,在我刪去自己的PEIM重新編譯OvmfPkg這個dsc之后,卻可以編譯通過。
百思不得其解。
接下來的編譯失敗的信息也少得可憐,也僅僅是告知我是我的PEIM模塊出了問題。。。。
build.py...
: error 7000: Failed to execute command
D:\Develop\Microsoft\VisualStudio\2019\Community\VC\Tools\MSVC\14.29.30133\bin\Hostx86\x86\nmake.exe /nologo tbuild [D:\edk2\edk2\Build\OvmfX64\DEBUG_VS2019\X64\OvmfPkg\MyHelloWorldInstallPpi\MyHelloWorldInstallPpi]
build.py...
: error F002: Failed to build module
D:\edk2\edk2\OvmfPkg\MyHelloWorldInstallPpi\MyHelloWorldInstallPpi.inf [X64, VS2019, DEBUG]
雖然始終找不到問題在哪里,但是可以確定是自己的問題,接下來就是開始漫長的排查。
下面介紹一下我的做法,供給后來的和我一樣的小白們參考/(ㄒoㄒ)/~~
Step 1、將.c文件中所有東西都注釋掉,僅僅保留 入口函數和return EFI_SUCCESS;語句

build一下,發現可以通過。
Step 2、將入口函數中的語句一行一行取消注釋。。。。。到了哪一句無法編譯通過,就是誰的問題。
后來終于定位到了,原來是這里DEBUG,不小心少復制了一個D

不得不吐槽,vscode 配合 EDK2原生的這個編譯器,真是個災難,編譯不通過什么提示都沒有。。。。定位這么小的錯誤需要半天!!!!!!!
vscode更是個大爛貨,這么明顯的錯誤都沒有提示~~~~
這個一句句的排查也只能夠是這種實驗的小模塊,如果是大工程,那就很耗費精力了。。。。(也許可以2分法排查?)
看來,寫一點編譯一點,這是一個好習慣。
少寫多編,少些多提交,始終是個習慣啊
補充
找到問題所在了,只是因為報錯信息過于靠上,往上面多翻一點就可以找到。
不太清楚為什么,沒有一編譯報錯就停止,猜測可能是Conf/Target.txt中默認開啟了多核編譯功能。





浙公網安備 33010602011771號