4、编写第一个UEFI程序:HelloWorld模块的创建、编译与运行

说实话,学UEFI开发最让人头疼的是什么?

不是代码有多难,而是你折腾了半天,连个"Hello World"都跑不出来。

我当年刚接触UEFI时,光搭建环境就花了两天。后来才发现,其实核心流程就那么几步。今天咱们就把这层窗户纸捅破。

4.1 准备工作:你需要什么?

在动手之前,先确认你手头有这些工具。我个人习惯用EDK2,这是Intel官方维护的UEFI开发套件,也是业界标准。

工具/组件 说明 获取方式
EDK2源码 UEFI开发框架,包含编译工具链和库 git clone https://github.com/tianocore/edk2.git
Python 3.x 用于运行编译脚本 官网下载或包管理器安装
Visual Studio 2019+ Windows下的C编译器(Linux用GCC) Visual Studio官网
QEMU模拟器 用于测试UEFI程序,不用真机 qemu.org下载
我的建议:初学者别一上来就用真机测试。我曾经在开发板上刷坏过两次固件,后来老老实实用QEMU。模拟器跑通了,再上真机,这是血的教训。

4.2 创建HelloWorld模块

EDK2里每个模块都有自己的文件夹。咱们先创建一个。

进入EDK2源码目录,找到 MdeModulePkg/Applications 这个路径。这里存放着各种示例应用。我们在这里新建一个文件夹,取名 HelloWorld。

然后在这个文件夹里创建三个文件:

  • HelloWorld.c —— 源代码文件
  • HelloWorld.inf —— 模块描述文件(告诉编译系统怎么编译)
  • HelloWorld.uni —— 字符串资源文件(可选,但建议加上)

4.3 编写源代码

打开 HelloWorld.c,写入以下代码:

#include <Uefi.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/UefiLib.h>

EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  Print(L"Hello, UEFI World!\n");
  Print(L"欢迎来到UEFI开发世界!\n");

  return EFI_SUCCESS;
}

这段代码很简单。每个UEFI应用的入口函数都是 UefiMain,它接收两个参数:ImageHandle 是模块自身的句柄,SystemTable 是系统表指针——通过它能调用UEFI提供的所有服务。

Print 函数是UEFI版的printf。注意前面的 L 前缀,表示宽字符串。UEFI内部统一使用Unicode编码。

关键点:UEFI程序没有main函数,只有UefiMain。返回EFI_SUCCESS表示正常退出。如果你返回其他错误码,系统会给出提示。

4.4 编写模块描述文件(INF)

INF文件是EDK2的"身份证"。它告诉编译系统:这个模块叫什么、用哪些源文件、依赖哪些库。

创建 HelloWorld.inf,内容如下:

[Defines]
  INF_VERSION                    = 0x00010006
  BASE_NAME                      = HelloWorld
  FILE_GUID                      = 12345678-1234-1234-1234-123456789abc
  MODULE_TYPE                    = UEFI_APPLICATION
  VERSION_STRING                 = 1.0
  ENTRY_POINT                    = UefiMain

[Sources]
  HelloWorld.c

[Packages]
  MdePkg/MdePkg.dec

[LibraryClasses]
  UefiApplicationEntryPoint
  UefiLib

这里有几个字段要注意:

  • FILE_GUID:每个UEFI模块必须有一个唯一的GUID。你可以用工具生成,也可以随便写一个(只要不冲突)。
  • MODULE_TYPE:这里填 UEFI_APPLICATION,表示这是一个独立的应用,不是驱动。
  • LibraryClasses:列出你用了哪些库。UefiApplicationEntryPoint 提供入口函数支持,UefiLib 提供Print等常用函数。
注意:GUID不能乱写,也不能重复。我曾经在一个项目里复制了别人的GUID,结果两个模块冲突,系统启动时直接蓝屏。排查了整整一个下午才发现是GUID重复了。

4.5 编译HelloWorld

编译前先设置环境变量。打开"x64 Native Tools Command Prompt for VS 2019"(或者你用的VS版本),然后执行:

cd C:\edk2
edksetup.bat
build -p MdeModulePkg\MdeModulePkg.dsc -m MdeModulePkg\Applications\HelloWorld\HelloWorld.inf

解释一下这条命令:

  • -p 指定平台描述文件(DSC),它定义了整个固件的模块组成。
  • -m 指定你要编译的模块路径。如果不加 -m,会编译整个平台,那得等很久。

编译成功后,你会在 Build\MdeModulePkg\DEBUG_VS2019x64\X64\ 目录下找到 HelloWorld.efi 文件。这就是最终的UEFI可执行程序。

小技巧:编译时加 -j 8 参数可以并行编译,速度翻倍。比如 build -j 8 -p ...。我一般开8个线程,老机器别开太多,容易卡死。

4.6 在QEMU中运行

有了 .efi 文件,怎么跑呢?最简单的方式是用QEMU模拟UEFI环境。

首先,你需要一个UEFI固件镜像。EDK2自带了一个叫 OVMF.fd 的文件,它就是UEFI固件的模拟版。在 edk2\Build\OvmfX64\DEBUG_VS2019x64\FV\ 下可以找到。

然后执行:

qemu-system-x86_64 -bios OVMF.fd -hda fat:rw:.\HelloWorld

这条命令的意思是:用OVMF作为BIOS,把当前目录下的 HelloWorld 文件夹模拟成一个硬盘。QEMU启动后,会自动进入UEFI Shell。

在Shell里输入:

fs0:
HelloWorld.efi

屏幕上就会打印出:

Hello, UEFI World!
欢迎来到UEFI开发世界!

看到这行输出,恭喜你——你的第一个UEFI程序跑起来了!

为什么用QEMU? 说白了,就是安全。你可以在QEMU里随便折腾,不会把真机搞坏。我刚开始学的时候,一天能重启QEMU上百次,要是真机早烧了。

4.7 核心逻辑流程图

下面这张图展示了从源码到运行的完整流程,我把它画成了SVG,方便你理解整个链路:

HelloWorld模块创建到运行流程图 步骤1:创建文件 HelloWorld.c / .inf / .uni 步骤2:编写代码 UefiMain入口 + Print输出 步骤3:编译 build命令生成.efi 步骤4:准备QEMU环境 获取OVMF.fd固件镜像 步骤5:启动QEMU qemu-system-x86_64命令 步骤6:进入UEFI Shell 自动进入Shell命令行 步骤7:运行HelloWorld.efi 输出结果 Hello, UEFI World! 欢迎来到UEFI开发世界!

这张图把整个流程串起来了。你从左上角开始,一步步往下走,最后在右下角看到输出结果。嗯,其实没那么复杂,对吧?

4.8 常见问题与避坑

我整理了几个新手最容易踩的坑,都是我用时间换来的经验:

  • 编译报错"找不到头文件":检查INF文件里的 [Packages] 段有没有写对。少一个包,编译就过不去。
  • QEMU启动后黑屏:大概率是OVMF.fd路径不对,或者没加 -bios 参数。我曾经忘了加 -bios,结果QEMU用默认的SeaBIOS启动,根本进不了UEFI Shell。
  • Shell里找不到.efi文件:检查 -hda fat:rw: 后面的路径。路径错了,QEMU就挂载不到你的文件。
  • Print输出乱码:字符串前面忘了加 L 前缀。UEFI用Unicode,不加L就是ASCII,打印出来全是问号。
特别提醒:如果你在真机上测试,千万别把 .efi 文件放到ESP分区(EFI System Partition)的根目录下。我曾经手滑覆盖了 bootx64.efi,结果系统直接起不来了。建议放到一个子目录里测试。

好了,第一个UEFI程序就讲到这里。你现在应该能自己创建、编译并运行一个HelloWorld了。这个流程虽然简单,但它是后面所有UEFI开发的基础。把这个流程跑通了,后面的驱动、协议、图形界面,都是在这个框架上叠加的。

可可精品资料库