010_用EDK2编译你的第一个UEFI程序HelloWorld
2026/5/14 1:14:04 网站建设 项目流程

用 EDK2 编译你的第一个 UEFI 程序:HelloWorld

🔥 UEFI/BSP 开发系列第 010 篇 | 难度:⭐ 入门

作者:BSP 开发工程师

系列目标:300 篇由浅入深,构建完整的 UEFI 固件知识体系


写在前面

前两篇我们在 Windows 和 Linux 上搭好了 EDK2 开发环境,编译出了 OVMF 固件。

但那是编译别人写好的代码。

今天,我们要写自己的代码——一个在 UEFI 环境中运行的 HelloWorld 程序

是的,这个程序不在 Windows 里运行,不在 Linux 里运行,它运行在操作系统启动之前,直接和固件对话。

如果你以前只写过跑在 OS 上面的程序,今天你将第一次触碰到"OS 之下"的世界。

很酷,对吧?


一、UEFI 程序和普通程序有什么不同?

先看一段普通 C 语言 HelloWorld:

#include<stdio.h>intmain(){printf("Hello, World!\n");return0;}

这段代码依赖stdio.h,依赖 C 标准库,依赖操作系统的系统调用。没有 OS,它跑不起来。

再看 UEFI 版本的 HelloWorld:

#include<Uefi.h>#include<Library/UefiApplicationEntryPoint.h>#include<Library/UefiLib.h>EFI_STATUS EFIAPIUefiMain(IN EFI_HANDLE ImageHandle,IN EFI_SYSTEM_TABLE*SystemTable){Print(L"Hello, UEFI World!\n");returnEFI_SUCCESS;}

几个关键区别:

维度普通 C 程序UEFI 程序
入口函数main()UefiMain()
运行环境操作系统上固件环境(无 OS)
依赖库C 标准库(libc)UEFI 库(MdePkg)
字符串类型char*(ASCII)CHAR16*(Unicode,L"…")
返回值intEFI_STATUS
参数argc/argvImageHandle + SystemTable

💡 UEFI 程序的入口函数接收两个参数:

  • ImageHandle:当前程序自己的"身份证"
  • SystemTable:系统服务表,通过它可以访问所有 UEFI 提供的功能(屏幕输出、内存分配、文件操作等)

Print(L"...")本质上是通过SystemTable->ConOut->OutputString()在屏幕上输出文字。UEFI 用的是 Unicode(UTF-16),所以字符串前面有个L


二、创建 HelloWorld 程序

我们需要创建两个文件:

  1. 源代码文件.c— 程序逻辑
  2. 模块描述文件.inf— 告诉 EDK2 构建系统怎么编译这个模块

2.1 创建目录

在 EDK2 的某个 Package 下创建我们的程序目录。初学者直接放在MdeModulePkg下最简单:

mkdir-p~/UEFI/edk2/MdeModulePkg/Application/HelloWorld

Windows 下对应路径:C:\UEFI\edk2\MdeModulePkg\Application\HelloWorld

2.2 创建源代码:HelloWorld.c

/** @file My first UEFI application - HelloWorld Copyright (c) 2024, My Name. All rights reserved. SPDX-License-Identifier: BSD-2-Clause-Patent **/#include<Uefi.h>#include<Library/UefiApplicationEntryPoint.h>#include<Library/UefiLib.h>#include<Library/UefiBootServicesTableLib.h>EFI_STATUS EFIAPIUefiMain(IN EFI_HANDLE ImageHandle,IN EFI_SYSTEM_TABLE*SystemTable){// 在屏幕上打印 Hello WorldPrint(L"=================================\n");Print(L" Hello, UEFI World!\n");Print(L" This is my first UEFI app.\n");Print(L"=================================\n");Print(L"\nPress any key to exit...\n");// 等待用户按下任意键EFI_INPUT_KEY Key;gBS->WaitForEvent(1,&gST->ConIn->WaitForKey,NULL);gST->ConIn->ReadKeyStroke(gST->ConIn,&Key);returnEFI_SUCCESS;}

💡代码解读

  • gST= Global System Table,就是入口函数的SystemTable参数的全局版本
  • gBS= Global Boot Services,启动阶段的服务集合
  • WaitForEvent让程序暂停,等待键盘输入事件
  • 这就像普通 C 程序里的getchar()system("pause")

2.3 创建模块描述文件:HelloWorld.inf

## @file # My first UEFI Application - HelloWorld # # Copyright (c) 2024, My Name. All rights reserved. # SPDX-License-Identifier: BSD-2-Clause-Patent ## [Defines] INF_VERSION = 0x00010005 BASE_NAME = HelloWorld FILE_GUID = a912f198-7f0e-4803-b908-b757b806ec83 MODULE_TYPE = UEFI_APPLICATION VERSION_STRING = 1.0 ENTRY_POINT = UefiMain [Sources] HelloWorld.c [Packages] MdePkg/MdePkg.dec MdeModulePkg/MdeModulePkg.dec [LibraryClasses] UefiApplicationEntryPoint UefiLib UefiBootServicesTableLib

INF 文件各段含义

INF 文件是 EDK2 中每个模块的"身份证"——每个 .c 文件想被编译,必须有一个对应的 .inf 文件

你可以把 INF 文件理解为 EDK2 版本的MakefileCMakeLists.txt,但它更偏向"声明式"——你只需要告诉构建系统"我叫什么、我依赖什么",具体怎么编译由 EDK2 的 build 工具自动处理。

段名作用类比
[Defines]模块的基本信息:名称、GUID、类型、入口函数CMake 的project()+add_executable()
[Sources]源代码文件列表CMake 的源文件列表
[Packages]依赖哪些包(.dec 文件)CMake 的find_package()
[LibraryClasses]依赖哪些库CMake 的target_link_libraries()

DSC 文件又是什么?DSC(Description file)是平台级别的总管文件——它定义了"整个固件由哪些模块组成"。一个.dsc对应一个平台(比如 OvmfPkgX64.dsc 对应 QEMU 虚拟机平台)。

简单说:INF 描述一个模块,DSC 把一堆模块组装成一个固件。后面第 062~064 篇会详细讲解这些文件。

⚠️FILE_GUID必须是唯一的。你可以用在线 GUID 生成器生成一个,也可以用 Python:

importuuid;print(str(uuid.uuid4()))

⚠️MODULE_TYPE = UEFI_APPLICATION表示这是一个 UEFI 应用程序(类似 EXE),不是驱动。


三、把程序加入编译

光创建了文件还不够,你需要告诉 EDK2 的构建系统:“嘿,我有一个新模块,请编译它。”

方法:修改 .dsc 文件

编辑 OVMF 平台的 DSC 文件,把我们的模块添加进去:

vim~/UEFI/edk2/OvmfPkg/OvmfPkgX64.dsc

找到[Components]段(文件末尾附近),添加一行:

[Components] # ... 已有的模块列表 ... MdeModulePkg/Application/HelloWorld/HelloWorld.inf # <-- 添加这一行

保存退出。


四、编译

# Linuxcd~/UEFI/edk2source.venv/bin/activatesourceedksetup.sh build
# Windows(VS 开发者命令行) cd C:\UEFI\edk2 edksetup.bat build

编译成功后,你的 HelloWorld 程序在这里:

# Linux Build/OvmfX64/DEBUG_GCC5/X64/HelloWorld.efi # Windows Build\OvmfX64\DEBUG_VS2022\X64\HelloWorld.efi

这个.efi文件就是你的 UEFI 可执行程序!它的格式是 PE32+(和 Windows 的 EXE 格式很像,但是跑在固件环境里)。


五、在 QEMU 中运行

5.1 创建一个虚拟磁盘

我们需要把 HelloWorld.efi 放到一个虚拟磁盘里,这样 UEFI Shell 才能找到它:

# 创建一个 FAT 格式的虚拟磁盘镜像ddif=/dev/zeroof=disk.imgbs=1Mcount=64mkfs.fat disk.img# 创建挂载点并挂载mkdir-p/tmp/uefi_disksudomountdisk.img /tmp/uefi_disk# 把 HelloWorld.efi 复制进去sudocpBuild/OvmfX64/DEBUG_GCC5/X64/HelloWorld.efi /tmp/uefi_disk/# 卸载sudoumount/tmp/uefi_disk

5.2 用 QEMU 启动

qemu-system-x86_64\-biosBuild/OvmfX64/DEBUG_GCC5/FV/OVMF.fd\-drivefile=disk.img,format=raw\-netnone\-serialstdio

5.3 在 UEFI Shell 中运行

QEMU 启动后进入 UEFI Shell,执行:

Shell> fs0: FS0:\> HelloWorld.efi

如果一切正常,你会看到:

================================= Hello, UEFI World! This is my first UEFI app. ================================= Press any key to exit...

🎉 恭喜!你成功运行了人生中第一个 UEFI 程序!

这段代码在操作系统启动之前就运行了。此刻没有 Windows,没有 Linux,只有你的代码和 UEFI 固件。


六、深入理解:这个程序到底经历了什么?

从你输入HelloWorld.efi到看到输出,UEFI 做了这些事:

你输入 HelloWorld.efi 并回车 ↓ UEFI Shell 在 FAT 文件系统中找到 HelloWorld.efi ↓ UEFI Image Loader 把 PE32+ 格式的 EFI 文件加载到内存 ↓ 检查文件头、分配内存、解析依赖 ↓ 跳转到入口函数 UefiMain() ↓ UefiMain 调用 Print() → 实际调用 SystemTable->ConOut->OutputString() ↓ GOP 驱动把字符渲染到屏幕上 ↓ WaitForEvent 等待键盘中断 ↓ 你按下任意键 → 函数返回 EFI_SUCCESS ↓ UEFI Shell 回收内存,回到命令行

你以为只是打印了一行字,其实背后有 Protocol 调用、内存管理、事件机制、驱动配合……这就是 UEFI 的面向对象架构在工作。


七、总结

维度内容
程序入口UefiMain(ImageHandle, SystemTable)
输出函数Print(L"...")gST->ConOut->OutputString()
文件格式.efi(PE32+ 格式)
模块描述.inf文件定义模块信息和依赖
编译方式修改 .dsc 添加模块 →build
运行方式QEMU + OVMF + UEFI Shell

下一篇:#011 UEFI Shell 是什么?它能干什么?——UEFI 固件里的"命令行工具",比你想的强大得多。

💬 如果这篇文章对你有帮助,欢迎关注本系列。300 篇 UEFI/BSP 系列文章持续更新中。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询