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

一、前言

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

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

三、环境搭建

    1. 下载 edk2-libc 源文件
    sh 复制代码
    git clone https://github.com/tianocore/edk2-libc.git

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

    text 复制代码
    yuan@ayuan-virtual-machine:~/src$ tree -L 1
    .
    ├── edk2/                  # 主仓库
    │   ├── MdePkg/
    │   ├── OvmfPkg/
    │   └── ...
    └── edk2-libc/             # libc 仓库
        ├── StdLib/
        ├── AppPkg/
        └── ...
    1. 配置环境变量
    sh 复制代码
    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 中看到类似这样的定义时:

    ini 复制代码
    [Packages]
      StdLib/StdLib.dec

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

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

附录:

为什么程序入口地址是 ShellCEntryLib

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

c 复制代码
int main(int argc, char **argv) {}

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

相关推荐
FreakStudio4 小时前
无硬件学LVGL:基于Web模拟器+MiroPython速通GUI开发—布局与空间管理篇
python·单片机·嵌入式·面向对象·并行计算·电子diy
左手厨刀右手茼蒿6 小时前
Linux 内核中的进程管理:从创建到终止
linux·嵌入式·系统内核
左手厨刀右手茼蒿6 小时前
Linux 内核中的 DMA 管理:从缓冲区到传输
linux·嵌入式·系统内核
隔壁大炮9 小时前
2.3 LED闪灯实验
嵌入式·硬件
带土118 小时前
6. exec函数族和守护进程
嵌入式
Freak嵌入式2 天前
MicroPython对接大模型:uopenai + 火山方舟实现文字聊天和图片理解
ide·驱动开发·ai·llm·嵌入式·micropython·upypi
凉、介2 天前
从设备树到驱动源码:揭秘嵌入式 Linux 中 MMC 子系统的统一与差异
linux·驱动开发·笔记·学习·嵌入式·sd·emmc
CinzWS2 天前
A53电源管理(下):DVFS与热管理的硬件实现——ARM芯片的“冷静艺术“
arm开发·嵌入式·芯片验证·原型验证·a53
CinzWS2 天前
QSPI协议 - 超越XIP:在内存映射、四线模式与DMA协同中压榨极致性能
嵌入式·qspi·芯片验证