如何创建一个 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文件打开终端
shcd /home/ayuan/src/edk2 source edksetup.sh编译 HelloWorldPkg
打开文件
./Conf/target.txt,修改如下项txtACTIVE_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 文件
shqemu-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 生成
Steady Progress!