总述
UEFI开发过程中,BIOS工程师主要关注点和工作都在于PEI和DXE阶段。
DXE阶段是我们的主战场,可以进行丰富且大量的功能驱动开发。
一阵见血。
我们换句话说,PEI阶段是进入DXE阶段前的一个不得已而为之的妥协,或是一个过渡的阶段,我们的目标是进入DXE阶段,能够放开拳脚。
下面介绍一下PEI(Pre-EFI Initialization,EFI前初始化),本人初学者,一家之言,如有错误请留言指正。
为什么有PEI阶段
在PEI阶段在SEC阶段之后,尽管进行了SEC的相关工作,但仍然相对初始。
尤其是内存仍然尚未初始化,而想要利用C语言来做一些丰富的功能开发,尽快进入DXE阶段,最关键的是能够大量地使用"栈
"。
因此在这个阶段,我们希望可以尽快能够初始化Memory
,在一些资料中也被称为"永久内存Permanent Memory"
。
此处的永久内存仍然是指Ram
,即断电易失的存储器
,永久是相对于SEC阶段中的Cache As Ram (CAR)
来说的。
在这个阶段仅利用 CPU 上的资源,如将 CPU 的缓存 Cache
作为栈,来调度PEIM(PEI Module)
,目的是最快进入DXE阶段。这些 PEIM
负责以下工作:
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
-
PEIM Dispatcher(调度器):具体地是在PeiCore中PeiDispatcher函数,Dispatcher会找出系统中的所有PEIM,并根据PEIM之间的依赖关系,按顺序执行PEIM。
-
PEI Foundation,即PeiCore,会建立一个 UEFI规范里叫 PEI Services Table 的变量,实际在代码里如下图中的gPs,该表对所有系统中的 PEIM 可见。通过PEI Services,PEIM 可以调用 PEI 阶段提供的一些系统功能,例如
Install PPI、Locate PPI
以及Notify PPI
等。
(另外说一嘴,在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分法排查?)
看来,写一点编译一点,这是一个好习惯。
少写多编,少些多提交,始终是个习惯啊