一、前言
通过阅读前面的 EDKII 相关代码实现,我们可以很容易发现其虽使用的是 C 语言语法,但编写规则与我们在 IDE 或者操作系统上运行的 C 语言代码不太一样。以简单的 HelloWorld 程序为例。EDKII 中的代码为:
c
#include <Uefi.h>
#include <Library/UefiLib.h>
EFI_STATUS
EFIAPI
UefiMain (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
) {
Print(L"Hello, World!\n");
return EFI_SUCCESS;
}
UNIX 风格代码如下:
c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
printf("Hello UEFI World from LibC!\n");
void* ptr = malloc(1024);
if (ptr) {
printf("Memory allocated successfully.\n");
free(ptr);
}
return 0;
}
对比以上两个程序,下面的代码可以使用我们常见的 C 语言库函数。EDKII 中不能使用类似 printf 之类的库函数就是因为 EDKII 工程中没有此类函数的实现,因此我们的任务就是引入 C 语言库函数的实现。
二、EDK2-LIBC
EDK2-LIBC (也称为 EADK ,即 EDK II Application Development Kit)是一个开源项目,该项目的目标就是降我们熟悉的 C 语言标准库移植到 UEFI 环境中,从能能够让我们在编写标准库程序的时候能够使用 printf、malloc 和 open 等函数。
为什么需要这个环境呢?虽然 UEFI 规范中提供了大量的 API 函数,但这些函数主要提供必要的硬件访问功能,无法使用很多已有的高级应用功能。举个例子来说,前面我们介绍了使用 UEFI 中的图形输出协议 GOP 在屏幕上显示一张图片,这种显示方式非常笨拙,本质上是通过控制屏幕上每个像素点的数据内容来实现,对不同协议的图像数据解析很不友好。但是 C 语言世界存在很多图片格式的解析代码,只需要我们直接调用即可。想象一下如果我们在 UEFI 中实现了这个 UNIX 风格的 C 语言环境,这些非常有空的功能就可以直接移植和调用了。
三、环境搭建
-
- 下载 edk2-libc 源文件
shgit clone https://github.com/tianocore/edk2-libc.git将 edk2-libc 与 edk2 放到同一层级路径中。
textyuan@ayuan-virtual-machine:~/src$ tree -L 1 . ├── edk2/ # 主仓库 │ ├── MdePkg/ │ ├── OvmfPkg/ │ └── ... └── edk2-libc/ # libc 仓库 ├── StdLib/ ├── AppPkg/ └── ... -
- 配置环境变量
shexport PACKAGES_PATH=$PWD/edk2:$PWD/edk2-libc配置这个环境变量的作用是告诉 EDK2 的构建系统去哪些额外的目录下寻找包(Packages)的源代码。需要注意的是,每次打开一个新的终端都需要执行一下这个命令。
如果你只进入
edk2目录运行build命令,构建系统只会解析edk2目录下的*.dsc(平台描述文件)和*.inf(模块描述文件)。它不知道edk2-libc的存在,因此当你编译一个需要标准 C 库的应用程序时,会报错"找不到 StdLib 包"。通过PACKAGES_PATH,构建系统会把edk2-libc也加入搜索路径。当你的应用*.inf文件中声明了StdLib或SocketLib时,构建系统就能在edk2-libc/StdLib目录下找到对应的头文件和实现代码。如果将来需要引用其他第三方 EDK2 包(如
edk2-platforms),只需继续用冒号:分隔路径追加到这个变量中即可。这种方法很方便并且不会污染到 EDK2 的源码文件。EDK2 构建系统(
build命令,实际是BaseTools)在解析依赖时,会按以下顺序查找包:- 工作区根目录(即
WORKSPACE环境变量指向的目录,通常是edk2的父目录)。 PACKAGES_PATH环境变量中列出的所有目录(按顺序查找)。EDK_TOOLS_PATH指定的目录(通常指向BaseTools)。
当在
edk2/OvmfPkg/OvmfPkg.dsc中看到类似这样的定义时:ini[Packages] StdLib/StdLib.dec构建系统就会依次去
PACKAGES_PATH里的每个目录下寻找StdLib文件夹及其中的StdLib.dec(包声明文件)。 -
- 编写三大文件
- C 程序源码:
c#include <stdio.h> #include <stdlib.h> int main(int argc, char **argv) { printf("Hello UEFI World from LibC!\n"); void* ptr = malloc(1024); if (ptr) { printf("Memory allocated successfully.\n"); free(ptr); } return 0; }- inf 文件
ini[Defines] INF_VERSION = 0x00010006 BASE_NAME = MyStdLibApp FILE_GUID = 4e397097-665f-4745-88c3-6305ac8623aa MODULE_TYPE = UEFI_APPLICATION VERSION_STRING = 1.0 ENTRY_POINT = ShellCEntryLib [Sources] MyStdLibApp.c [Packages] MdePkg/MdePkg.dec ShellPkg/ShellPkg.dec StdLib/StdLib.dec [LibraryClasses] UefiApplicationEntryPoint UefiLib UefiBootServicesTableLib LibC LibStdio ShellCEntryLib- dsc 文件
ini[Defines] PLATFORM_NAME = MyPkg PLATFORM_GUID = 87654321-4321-4321-4321-CBA987654321 PLATFORM_VERSION = 1.0 DSC_SPECIFICATION = 0x00010005 OUTPUT_DIRECTORY = Build/MyPkg SUPPORTED_ARCHITECTURES = X64 BUILD_TARGETS = DEBUG|RELEASE [LibraryClasses] UefiLib|MdePkg/Library/UefiLib/UefiLib.inf UefiApplicationEntryPoint|MdePkg/Library/UefiApplicationEntryPoint/UefiApplicationEntryPoint.inf PrintLib|MdePkg/Library/BasePrintLib/BasePrintLib.inf PcdLib|MdePkg/Library/BasePcdLibNull/BasePcdLibNull.inf MemoryAllocationLib|MdePkg/Library/UefiMemoryAllocationLib/UefiMemoryAllocationLib.inf DebugLib|MdePkg/Library/UefiDebugLibConOut/UefiDebugLibConOut.inf BaseMemoryLib|MdePkg/Library/BaseMemoryLib/BaseMemoryLib.inf BaseLib|MdePkg/Library/BaseLib/BaseLib.inf UefiBootServicesTableLib|MdePkg/Library/UefiBootServicesTableLib/UefiBootServicesTableLib.inf DevicePathLib|MdePkg/Library/UefiDevicePathLib/UefiDevicePathLib.inf UefiRuntimeServicesTableLib|MdePkg/Library/UefiRuntimeServicesTableLib/UefiRuntimeServicesTableLib.inf RegisterFilterLib|MdePkg/Library/RegisterFilterLibNull/RegisterFilterLibNull.inf DebugPrintErrorLevelLib|MdePkg/Library/BaseDebugPrintErrorLevelLib/BaseDebugPrintErrorLevelLib.inf # 解决 HiiLib 缺失问题 HiiLib|MdeModulePkg/Library/UefiHiiLib/UefiHiiLib.inf UefiHiiServicesLib|MdeModulePkg/Library/UefiHiiServicesLib/UefiHiiServicesLib.inf # 解决 UefiShellLib 相关的其他潜在缺失 ShellLib|ShellPkg/Library/UefiShellLib/UefiShellLib.inf FileHandleLib|MdePkg/Library/UefiFileHandleLib/UefiFileHandleLib.inf SortLib|MdeModulePkg/Library/UefiSortLib/UefiSortLib.inf !include StdLib/StdLib.inc [Components] MyPkg/Application/MyStdLibApp/MyStdLibApp.inf -
- 编译
shbuild -p edk2/MyPkg/MyPkg.dsc
附录:
为什么程序入口地址是 ShellCEntryLib?
回忆一下我们在写Linux应用程序的时候是怎么接受命令行命令和参数的?
c
int main(int argc, char **argv) {}
是不是跟我们上面的示例程序完全一致。也就是说使用ShellCEntryLib入口函数的目的就是为了使应用程序能够接收命令行参数,以对不同参数做不同处理。也就是说,表面上我们输入的命令行参数似乎是直接进入了我们编写的 UNIX 应用程序的 main 函数中,实际不是这样。真实情况是首先进入入口函数 ShellCEntryLib,然后在入口函数中借用 shell 相关 Protocol 接收参数并调用用户定义的 ShellAppMain 把参数传入这个函数。edk2-libc 对 ShellAppMain 又做了一层包装,在 ShellAppMain 又调用了真正是我们自己定义的 main 函数。
ShellCEntryLib 的实现非常简洁(位于 ShellPkg/Library/UefiShellCEntryLib/UefiShellCEntryLib.c):
c
EFI_STATUS
EFIAPI
ShellCEntryLib (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
INTN ReturnFromMain;
EFI_SHELL_PARAMETERS_PROTOCOL *EfiShellParametersProtocol = NULL;
EFI_SHELL_INTERFACE *EfiShellInterface = NULL;
EFI_STATUS Status;
// 优先尝试 Shell 2.0 接口(推荐)
Status = SystemTable->BootServices->OpenProtocol(
ImageHandle,
&gEfiShellParametersProtocolGuid,
(VOID **)&EfiShellParametersProtocol,
ImageHandle, NULL,
EFI_OPEN_PROTOCOL_GET_PROTOCOL);
if (!EFI_ERROR(Status)) {
ReturnFromMain = ShellAppMain(
EfiShellParametersProtocol->Argc,
EfiShellParametersProtocol->Argv);
} else {
// 兼容旧版 Shell 1.0 接口
Status = SystemTable->BootServices->OpenProtocol(
ImageHandle,
&gEfiShellInterfaceGuid,
(VOID **)&EfiShellInterface,
ImageHandle, NULL,
EFI_OPEN_PROTOCOL_GET_PROTOCOL);
if (!EFI_ERROR(Status)) {
// 重点:在这里调用用户定义的 ShellAppMain 入口函数并传入命令行参数
ReturnFromMain = ShellAppMain(
EfiShellInterface->Argc,
EfiShellInterface->Argv);
} else {
ASSERT(FALSE); // 没有 Shell 环境
}
}
return ReturnFromMain; // INTN 会被隐式转换为 EFI_STATUS
}
| 入口方式 | 函数签名 | 参数处理 | 适合场景 |
|---|---|---|---|
| 标准 UEFI | EFI_STATUS EFIAPI UefiMain(ImageHandle, SystemTable) |
需手动 OpenProtocol 获取参数 | 简单应用、不依赖 Shell |
| ShellCEntryLib | INTN EFIAPI ShellAppMain(Argc, Argv) |
自动获取 | Shell 命令行工具、libc 应用 |
| edk2-libc + main | int main(int argc, char **argv) |
内部再包装 | 最接近 Unix C 风格 |
*表格由 Gemini 生成。
Steady Progress!