如何创建一个 UEFI 应用程序

在之前的文章中曾详细介绍了 EDKII 开发环境的搭建以及 OVMF 固件的编译过程。并且使用 QEMU 虚拟机来执行编译好的 OVMF 固件。我们知道在 Linux 终端中可以在命令行中执行编译好的应用程序,UEFI 也有 shell,如下图所示。我们能够在 shell 中执行编译好的 UEFI Application。本文以简单的 Hello World 程序为例来介绍 UEFI 应用程序的编译执行过程和各个文件的作用。

1. 编译并执行一个 Hello World 程序

  • 在 EDKII 目录下创建文件 HelloWorldPkg

    image-20260331213440637

  • 创建文件 HelloWorld.c

    #include 
    #include 
    
    EFI_STATUS 
    EFIAPI
    UefiMain (
        IN EFI_HANDLE ImageHandle,
        IN EFI_SYSTEM_TABLE *SystemTable
    ) {
        Print(L"Hello, World!\n");
        return EFI_SUCCESS;
    }
    
  • 创建文件 HelloWorld.inf

    GUID 可通过网站产生:https://guidgen.com/

    [Defines]
        INF_VERSION = 0x00010006
        BASE_NAME = HelloWorld
        FILE_GUID = 69ea2943-dbdd-404c-a3bf-6ef3fdfdf0a1
        MODULE_TYPE = UEFI_APPLICATION
        VERSION_STRING = 1.0
        ENTRY_POINT = UefiMain
    
    [Sources]
        HelloWorld.c
    
    [Packages]
        MdePkg/MdePkg.dec
    
    [LibraryClasses]
        UefiApplicationEntryPoint
        UefiLib
    
  • 创建文件 HelloWorldPkg.dsc

    [Defines]
        PLATFORM_NAME = HelloWorldPkg
        PLATFORM_GUID = 0adf0da5-100e-49a9-9f87-76215486216d
        PLATFORM_VERSION = 0.1
        DSC_SPECIFICATION = 0x00010005
        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
    
    [Components]
        HelloWorldPkg/HelloWorld.inf
    
  • 编译为 .efi 文件

    打开终端

    cd /home/ayuan/src/edk2
    source edksetup.sh
    

    编译 HelloWorldPkg

    打开文件 ./Conf/target.txt,修改如下项

    ACTIVE_PLATFORM       = HelloWorldPkg/HelloWorldPkg.dsc
    TARGET                = DEBUG
    TARGET_ARCH           = X64
    TOOL_CHAIN_TAG        = GCC5
    

    回到终端执行命令 build,生成的 efi 文件的路径如下:

    /home/ayuan/src/edk2/Build/HelloWorldPkg/DEBUG_GCC5/X64/HelloWorld.efi
    
  • 在 QEMU 中打开 OVMF 固件,然后在 UEFI Shell 中执行刚才编译的 HelloWorld.efi 文件

    qemu-system-x86_64 -bios /home/ayuan/run-ovmf/OVMF.fd -drive format=raw,file=fat:rw:/home/ayuan/run-ovmf/hda-contents -m 512M
    
    # 或者
    qemu-system-x86_64 -m 512M -drive if=pflash,format=raw,readonly=on,file=/home/ayuan/run-ovmf/OVMF.fd -drive if=pflash,format=raw,file=fat:rw:/home/ayuan/run-ovmf/hda-contents
    

    image-20260331215315760

除了 .c 源文件之外,我们还涉及到 INF, DSC, DEC 三个重要的文件。三个文件分别用于描述“模块”,“平台”,和“包”。他们的关系如下所示:

平台 (Platform)
   └── 由多个 模块 (Module) 组成
          ├── 来自 包A (Package A)
          ├── 来自 包B (Package B)
          └── 来自 包C (Package C)

1. 包(Package)是“资源提供者” 一个包就是一个功能或主题相关的“大仓库”。 例如:

  • MdePkg:最基础的库、头文件、通用协议
  • MdeModulePkg:通用驱动(如控制台、文件系统、USB 等)
  • OvmfPkg:专用于 QEMU/Ovmf 虚拟机的平台包
  • ShellPkg:UEFI Shell 相关模块

包通过 .DEC 文件对外声明:我提供了哪些头文件、哪些库、哪些 GUID、哪些 PCD。

2. 模块(Module)是“可构建单元” 模块是真正会被编译的东西(.efi、.lib)。 每个模块 必须属于某个包,它的 .INF 文件第一件事就是通过 [Packages] 节声明自己属于哪些包,从而获得头文件和定义。 一个 ConOutDxe.inf(控制台输出驱动)属于 MdeModulePkg 这个包。

3. 平台(Platform)是“最终产品组装者” 平台负责决定:“我这个主板/产品要用哪些模块?” 它通过 .DSC 文件的 [Components] 节,把来自不同包的各种模块“挑选”进来,并配置 PCD 值、库映射关系等。 最终通过构建命令生成完整的固件映像。

三个文件的相互引用关系如下:

在 .INF 文件中必须说明引用包,就是当前模块的源码使用了哪些包定义的函数或者接口:

[Packages]
  MdePkg/MdePkg.dec
  MdeModulePkg/MdeModulePkg.dec

在 .DSC 文件中需要引用模块,就是需要将那些模块编译进该平台,如:

[Components]
  MdeModulePkg/Universal/Console/ConOutDxe/ConOutDxe.inf
  OvmfPkg/QemuVideoDxe/QemuVideoDxe.inf

读者可能注意到我们在 HelloWorldPkg 中并没有创建 DEC 文件,这是因为没有其他模块使用到我们自定义的这个包,所以不创建也没什么问题。

2. INF 文件说明

INF 文件是单个模块(Module)的“身份证”。一个模块可以是驱动(Driver)、库(Library)、应用(Application)或 PEI/DXE 模块等。它告诉构建系统这个模块由哪些源文件组成,依赖哪些包、库、协议、GUID,模块的类型、入口点、输出文件名是什么,编译时需要哪些特殊选项等。没有 INF 文件,模块就无法被构建。每个 .inf 文件对应一个独立的、可独立构建的单元(最终生成 .efi 或 .lib)。INF 通常放在模块目录下(如 MdeModulePkg/Universal/Console/ConOutDxe/ConOutDxe.inf)。

INF 文件的常用组成:

  • [Defines]:模块基本信息(版本、GUID、类型、入口点等)。
  • [Packages]:依赖哪些包(提供头文件和 PCD)。
  • [Sources]:源代码文件列表。
  • [LibraryClasses]:需要链接哪些库类。
  • [Protocols] / [Guids] / [Ppis]:使用的协议/GUID/PPI 及使用方式(BY_START、PRODUCES 等)。
  • [BuildOptions]:特定编译器选项。
  • [Depex](可选):DXE 依赖表达式。

例如:

[Defines]
  INF_VERSION                    = 1.27
  BASE_NAME                      = HelloWorld
  FILE_GUID                      = 12345678-ABCD-1234-ABCD-123456789ABC
  MODULE_TYPE                    = UEFI_APPLICATION   # 或 DXE_DRIVER、BASE 等
  VERSION_STRING                 = 1.0
  ENTRY_POINT                    = UefiMain          # 入口函数名

[Packages]
  MdePkg/MdePkg.dec
  MdeModulePkg/MdeModulePkg.dec

[Sources]
  HelloWorld.c
  HelloWorld.h

[LibraryClasses]
  UefiLib
  UefiApplicationEntryPoint
  DebugLib

[Protocols]
  gEfiShellProtocolGuid           ## CONSUMES

[Guids]
  gEfiMdeModulePkgTokenSpaceGuid  ## SOMETIMES_PRODUCES

3. DSC 文件说明

DSC 为平台描述文件,是整个平台(Platform)的“构建蓝图”。它定义了这个平台要包含哪些模块(INF 文件),库类(LibraryClass)如何映射到具体实现,PCD(Platform Configuration Database)值如何覆盖,平台整体的架构、构建目标、输出目录等。一个平台通常只有一个主 DSC 文件(如 OvmfPkg/OvmfPkgX64.dsc 或 PlatformPkg/Platform.dsc)。DSC 不负责包的内容声明(那是 DEC),也不负责 Flash 布局(那是 FDF),但会引用 FDF 来生成最终固件映像。

DSC 文件常用组成:

[Defines]:平台名称、GUID、支持架构、构建目标等。

[LibraryClasses]:库类 → 具体 INF 的映射(全局生效)。

[Pcds]:覆盖包中声明的 PCD 默认值(FixedAtBuild、Dynamic 等)。

[Components]:列出所有要构建的模块 INF 文件(支持条件编译)。

[Components.IA32] / [Components.X64] 等架构特定节。

例如:

[Defines]
  PLATFORM_NAME                  = MyPlatform
  PLATFORM_GUID                  = 87654321-ABCD-1234-ABCD-123456789ABC
  PLATFORM_VERSION               = 1.0
  DSC_SPECIFICATION              = 1.28
  OUTPUT_DIRECTORY               = Build/MyPlatform
  SUPPORTED_ARCHITECTURES        = IA32|X64
  BUILD_TARGETS                  = DEBUG|RELEASE
  SKUID_IDENTIFIER               = DEFAULT

[LibraryClasses]
  DebugLib| MdePkg/Library/BaseDebugLibSerialPort/BaseDebugLibSerialPort.inf
  UefiLib| MdePkg/Library/UefiLib/UefiLib.inf
  # ... 其他库映射

[PcdsFixedAtBuild]
  gEfiMdeModulePkgTokenSpaceGuid.PcdHelloWorldPrintTimes| 5 | UINT32 | 0x40000005

[Components]
  # 核心模块
  MdeModulePkg/Universal/Console/ConOutDxe/ConOutDxe.inf
  MyPkg/HelloWorld/HelloWorld.inf   # 引用上面的 INF

[Components.X64]
  # 只在 X64 下构建的模块
  OvmfPkg/QemuVideoDxe/QemuVideoDxe.inf

4. DEC 文件说明

DEC 是包(Package)的“目录索引”。一个包是一组相关模块、库、头文件、GUID、协议、PCD 的集合(如 MdePkg、MdeModulePkg、OvmfPkg)。DEC 文件的作用是声明包对外提供什么(GUID、Protocol、PPI、LibraryClass、PCD),指定头文件包含路径([Includes]),让其他模块的 INF 文件可以通过 [Packages] 引用这个包,从而获得头文件和 PCD 定义。

没有 DEC,模块就无法知道这个包里有哪些可用的接口和配置。

DEC 文件常用组成:

[Defines]:包名称、GUID、版本。

[Includes]:头文件目录(支持架构特定)。

[LibraryClasses]:包提供的库类及其头文件路径。

[Guids] / [Protocols] / [Ppis]:声明 GUID/协议/PPI(带注释说明用途)。

[Pcds]:声明所有 PCD(FeatureFlag、FixedAtBuild、Dynamic 等)及其默认值、类型、Token。

例如:

[Defines]
  DEC_SPECIFICATION              = 1.27
  PACKAGE_NAME                   = MdePkg
  PACKAGE_GUID                   = 1E0A9C1A-5A9C-4C9A-9B7A-5A9C1E0A9C1A
  PACKAGE_VERSION                = 1.05

[Includes]
  Include
  Include/Ia32                   # 架构特定

[LibraryClasses]
  ## @libraryclass 基础内存操作库
  BaseMemoryLib| Include/Library/BaseMemoryLib.h

[Guids]
  ## Include/Guid/MdePkgTokenSpace.h
  gEfiMdePkgTokenSpaceGuid = { 0x1E0A9C1A, 0x5A9C, 0x4C9A, {0x9B, 0x7A, 0x5A, 0x9C, 0x1E, 0x0A, 0x9C, 0x1A} }

[PcdsFixedAtBuild, PcdsPatchableInModule, PcdsDynamic, PcdsDynamicEx]
  ## 此 PCD 定义 HelloWorld 打印次数
  # @Prompt HelloWorld print times.
  gEfiMdePkgTokenSpaceGuid.PcdHelloWorldPrintTimes|1|UINT32|0x40000005

*示例代码由 grok 生成
本文参考:https://www.bilibili.com/video/BV17h411x7Wr?spm_id_from=333.788.videopod.sections&vd_source=2ee7caa81fced5c94d0d863e82c6acae


Steady Progress!


原文地址: https://www.cveoy.top/t/topic/qGj0 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录