当前位置: 首页 > news >正文

如何在EDKII中编译UNIX风格C语言

一、前言

通过阅读前面的 EDKII 相关代码实现,我们可以很容易发现其虽使用的是 C 语言语法,但编写规则与我们在 IDE 或者操作系统上运行的 C 语言代码不太一样。以简单的 HelloWorld 程序为例。EDKII 中的代码为:

#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 风格代码如下:

#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 环境中,从能能够让我们在编写标准库程序的时候能够使用printfmallocopen等函数。

为什么需要这个环境呢?虽然 UEFI 规范中提供了大量的 API 函数,但这些函数主要提供必要的硬件访问功能,无法使用很多已有的高级应用功能。举个例子来说,前面我们介绍了使用 UEFI 中的图形输出协议 GOP 在屏幕上显示一张图片,这种显示方式非常笨拙,本质上是通过控制屏幕上每个像素点的数据内容来实现,对不同协议的图像数据解析很不友好。但是 C 语言世界存在很多图片格式的解析代码,只需要我们直接调用即可。想象一下如果我们在 UEFI 中实现了这个 UNIX 风格的 C 语言环境,这些非常有空的功能就可以直接移植和调用了。

三、环境搭建

    1. 下载 edk2-libc 源文件
    git clone https://github.com/tianocore/edk2-libc.git

    将 edk2-libc 与 edk2 放到同一层级路径中。

    yuan@ayuan-virtual-machine:~/src$ tree -L 1
    .
    ├── edk2/ # 主仓库
    │ ├── MdePkg/
    │ ├── OvmfPkg/
    │ └── ...
    └── edk2-libc/ # libc 仓库
    ├── StdLib/
    ├── AppPkg/
    └── ...
    1. 配置环境变量
    export PACKAGES_PATH=$PWD/edk2:$PWD/edk2-libc

    配置这个环境变量的作用是告诉 EDK2 的构建系统去哪些额外的目录下寻找包(Packages)的源代码。需要注意的是,每次打开一个新的终端都需要执行一下这个命令。

    如果你只进入edk2目录运行build命令,构建系统只会解析edk2目录下的*.dsc(平台描述文件)和*.inf(模块描述文件)。它不知道edk2-libc的存在,因此当你编译一个需要标准 C 库的应用程序时,会报错“找不到 StdLib 包”。通过PACKAGES_PATH,构建系统会把edk2-libc也加入搜索路径。当你的应用*.inf文件中声明了StdLibSocketLib时,构建系统就能在edk2-libc/StdLib目录下找到对应的头文件和实现代码。

    如果将来需要引用其他第三方 EDK2 包(如edk2-platforms),只需继续用冒号:分隔路径追加到这个变量中即可。这种方法很方便并且不会污染到 EDK2 的源码文件。

    EDK2 构建系统(build命令,实际是BaseTools)在解析依赖时,会按以下顺序查找包:

    • 工作区根目录(即WORKSPACE环境变量指向的目录,通常是edk2的父目录)。
    • PACKAGES_PATH环境变量中列出的所有目录(按顺序查找)。
    • EDK_TOOLS_PATH指定的目录(通常指向BaseTools)。

    当在edk2/OvmfPkg/OvmfPkg.dsc中看到类似这样的定义时:

    [Packages]
    StdLib/StdLib.dec

    构建系统就会依次去PACKAGES_PATH里的每个目录下寻找StdLib文件夹及其中的StdLib.dec(包声明文件)。

    1. 编写三大文件
    • 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 文件
    [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 文件
    [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
    1. 编译
    build -p edk2/MyPkg/MyPkg.dsc

附录:

为什么程序入口地址是ShellCEntryLib

回忆一下我们在写Linux应用程序的时候是怎么接受命令行命令和参数的?

int main(int argc, char **argv) {}

是不是跟我们上面的示例程序完全一致。也就是说使用ShellCEntryLib入口函数的目的就是为了使应用程序能够接收命令行参数,以对不同参数做不同处理。也就是说,表面上我们输入的命令行参数似乎是直接进入了我们编写的 UNIX 应用程序的main函数中,实际不是这样。真实情况是首先进入入口函数ShellCEntryLib,然后在入口函数中借用 shell 相关 Protocol 接收参数并调用用户定义的ShellAppMain把参数传入这个函数。edk2-libcShellAppMain又做了一层包装,在ShellAppMain又调用了真正是我们自己定义的main函数。

ShellCEntryLib的实现非常简洁(位于ShellPkg/Library/UefiShellCEntryLib/UefiShellCEntryLib.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
}
入口方式函数签名参数处理适合场景
标准 UEFIEFI_STATUS EFIAPI UefiMain(ImageHandle, SystemTable)需手动 OpenProtocol 获取参数简单应用、不依赖 Shell
ShellCEntryLibINTN EFIAPI ShellAppMain(Argc, Argv)自动获取Shell 命令行工具、libc 应用
edk2-libc + mainint main(int argc, char **argv)内部再包装最接近 Unix C 风格
http://www.jsqmd.com/news/1138063/

相关文章:

  • 计算机毕业设计之基于SSM框架的高校数码学习用品租赁管理系统的设计与实现
  • P.NET MVC]Contact Manager开发之旅之迭代2 - 修改样式,美化应用
  • 文案改写润色 GPT5.5 低价接口推荐
  • PowerToys Text Extractor:你的屏幕文字提取终极解决方案
  • 锂离子电池过压保护与BQ29200二级保护方案详解
  • DocMate实战指南:从零开始构建openEuler项目文档的完整流程
  • BedrockLauncher:Minecraft基岩版的技术架构解构与多版本管理解决方案
  • 探秘Windows USB驱动安装的3大黑科技:libwdi深度解密
  • Claude Code 多智能体编排与 Workflow 实战:解锁 AI 协作的力量
  • 结构化方法:自顶向下的“拆解术“
  • 如何快速上手uwal:从安装到部署的完整指南
  • kmpi安全最佳实践指南:确保HPC应用的数据传输安全与高效
  • GBase 8a之gcrcman在非原集群上恢复时遇到拓扑结构检查不一致的问题及其解决方案
  • AI/ML工程师的数学基石:概率统计、线性代数与微积分的完整学习路线图
  • 弯弦:散文
  • 【华为OD机试真题 新系统】1038、SQL记录拆分 | 机试真题+思路参考+代码解析(C++、Java、Py、C语言、JS)
  • 生成式表单 Schema:字段能生成,校验也要跟上
  • SecHex-Spoofy深度解析:专业级HWID修改工具实战指南
  • Orbbec SDK v2.8.7 与 OpenNI2 v2.3.0.86 对比:3个关键差异与5款相机兼容性分析
  • 从选题到定稿全流程 AI 期刊论文工具,文献综述、实证分析自动生成
  • ICM-42688-P与PIC18F27K42在工业自动化中的高效组合
  • 并发编程核心概念辨析
  • 免费前端开发库认证终极指南:从零到专业的三步进阶法
  • 131.IEC61131-3 标准编程!CODESYS/TwinCAT 通用分拣控制工程方案
  • 什么是软件?别再说“就是代码”了,那只是半成品
  • Java金融系统银行级安全加固实战指南:从代码到运维的纵深防御
  • STM32L081CB驱动WS2812 LED灯带的实战指南
  • 别被“函数”吓住——C语言函数,其实就是你的“万能小工具”
  • 如何使用Conda国内源安装虚拟环境(保姆级全平台教程)
  • 5步完成老Mac系统升级:OpenCore Legacy Patcher终极指南