如何使用 UEFI Shell 执行 Hello World 程序

如何创建一个 UEFI 应用程序

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

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

  • 在 EDKII 目录下创建文件 HelloWorldPkg

  • 创建文件 HelloWorld.c

    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;
    }
  • 创建文件 HelloWorld.inf

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

    ini 复制代码
    [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

    ini 复制代码
    [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 文件

    打开终端

    sh 复制代码
    cd /home/ayuan/src/edk2
    source edksetup.sh

    编译 HelloWorldPkg

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

    txt 复制代码
    ACTIVE_PLATFORM       = HelloWorldPkg/HelloWorldPkg.dsc
    TARGET                = DEBUG
    TARGET_ARCH           = X64
    TOOL_CHAIN_TAG        = GCC5

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

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

    sh 复制代码
    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

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

text 复制代码
平台 (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 文件中必须说明引用包,就是当前模块的源码使用了哪些包定义的函数或者接口:

ini 复制代码
[Packages]
  MdePkg/MdePkg.dec
  MdeModulePkg/MdeModulePkg.dec

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

ini 复制代码
[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、类型、入口点等)。

  • Sources\]:源代码文件列表。

  • Protocols\] / \[Guids\] / \[Ppis\]:使用的协议/GUID/PPI 及使用方式(BY_START、PRODUCES 等)。

  • Depex\](可选):DXE 依赖表达式。

ini 复制代码
[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] 等架构特定节。

例如:

ini 复制代码
[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。

例如:

ini 复制代码
[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!

相关推荐
牛奶咖啡132 天前
DevOps自动化运维实践_搭建UEFI网络引导的自动安装Debian系统
运维·自动化·devops·uefi·pxe·debian自动应答文件·debian网络自动化安装系统
牛奶咖啡135 天前
DevOps自动化运维实践_基于Cobbler搭建UEFI网络引导的自动安装平台
linux·运维·自动化·uefi·pxe·uefi网络引导自动安装平台·tftp dhcp 环境搭建
Felven13 天前
飞腾平台 UEFI 与 U-Boot 启动方案对比及选型建议
运维·uefi·uboot·飞腾
proware1 个月前
edp极化问题解决之uefi篇
uefi·edid
yao000372 个月前
基于QEMU+OpenSBI+edk2的riscv启动流程解析
qemu·riscv·uefi·bios·固件·opensbi
tianyuanwo4 个月前
深度解析:Linux ISO引导配置与安装模式设计
linux·uefi·iso·isolinux.cfg·grub.cfg
阿源-4 个月前
UEFI 中的杂项知识总结-Protocol Handle 机制的详细介绍
嵌入式·uefi·edk2·固件
REDcker4 个月前
UEFI BIOS深度解析:现代固件架构的革命性突破
架构·操作系统·uefi·bios
阿源-4 个月前
UEFI - FV/FFS/FDF 的关系
嵌入式·uefi·edk2·固件