用 EDK2 编译你的第一个 UEFI 程序HelloWorld UEFI/BSP 开发系列第 010 篇 | 难度⭐ 入门作者BSP 开发工程师系列目标300 篇由浅入深构建完整的 UEFI 固件知识体系写在前面前两篇我们在 Windows 和 Linux 上搭好了 EDK2 开发环境编译出了 OVMF 固件。但那是编译别人写好的代码。今天我们要写自己的代码——一个在 UEFI 环境中运行的 HelloWorld 程序。是的这个程序不在 Windows 里运行不在 Linux 里运行它运行在操作系统启动之前直接和固件对话。如果你以前只写过跑在 OS 上面的程序今天你将第一次触碰到OS 之下的世界。很酷对吧一、UEFI 程序和普通程序有什么不同先看一段普通 C 语言 HelloWorld#includestdio.hintmain(){printf(Hello, World!\n);return0;}这段代码依赖stdio.h依赖 C 标准库依赖操作系统的系统调用。没有 OS它跑不起来。再看 UEFI 版本的 HelloWorld#includeUefi.h#includeLibrary/UefiApplicationEntryPoint.h#includeLibrary/UefiLib.hEFI_STATUS EFIAPIUefiMain(IN EFI_HANDLE ImageHandle,IN EFI_SYSTEM_TABLE*SystemTable){Print(LHello, UEFI World!\n);returnEFI_SUCCESS;}几个关键区别维度普通 C 程序UEFI 程序入口函数main()UefiMain()运行环境操作系统上固件环境无 OS依赖库C 标准库libcUEFI 库MdePkg字符串类型char*ASCIICHAR16*UnicodeL…返回值intEFI_STATUS参数argc/argvImageHandle SystemTable UEFI 程序的入口函数接收两个参数ImageHandle当前程序自己的身份证SystemTable系统服务表通过它可以访问所有 UEFI 提供的功能屏幕输出、内存分配、文件操作等Print(L...)本质上是通过SystemTable-ConOut-OutputString()在屏幕上输出文字。UEFI 用的是 UnicodeUTF-16所以字符串前面有个L。二、创建 HelloWorld 程序我们需要创建两个文件源代码文件.c— 程序逻辑模块描述文件.inf— 告诉 EDK2 构建系统怎么编译这个模块2.1 创建目录在 EDK2 的某个 Package 下创建我们的程序目录。初学者直接放在MdeModulePkg下最简单mkdir-p~/UEFI/edk2/MdeModulePkg/Application/HelloWorldWindows 下对应路径C:\UEFI\edk2\MdeModulePkg\Application\HelloWorld2.2 创建源代码HelloWorld.c/** file My first UEFI application - HelloWorld Copyright (c) 2024, My Name. All rights reserved. SPDX-License-Identifier: BSD-2-Clause-Patent **/#includeUefi.h#includeLibrary/UefiApplicationEntryPoint.h#includeLibrary/UefiLib.h#includeLibrary/UefiBootServicesTableLib.hEFI_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 UefiBootServicesTableLibINF 文件各段含义INF 文件是 EDK2 中每个模块的身份证——每个 .c 文件想被编译必须有一个对应的 .inf 文件。你可以把 INF 文件理解为 EDK2 版本的Makefile或CMakeLists.txt但它更偏向声明式——你只需要告诉构建系统我叫什么、我依赖什么具体怎么编译由 EDK2 的 build 工具自动处理。段名作用类比[Defines]模块的基本信息名称、GUID、类型、入口函数CMake 的project()add_executable()[Sources]源代码文件列表CMake 的源文件列表[Packages]依赖哪些包.dec 文件CMake 的find_package()[LibraryClasses]依赖哪些库CMake 的target_link_libraries()那DSC 文件又是什么DSCDescription file是平台级别的总管文件——它定义了整个固件由哪些模块组成。一个.dsc对应一个平台比如 OvmfPkgX64.dsc 对应 QEMU 虚拟机平台。简单说INF 描述一个模块DSC 把一堆模块组装成一个固件。后面第 062~064 篇会详细讲解这些文件。⚠️FILE_GUID必须是唯一的。你可以用在线 GUID 生成器生成一个也可以用 Pythonimportuuid;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# WindowsVS 开发者命令行 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/zeroofdisk.imgbs1Mcount64mkfs.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_disk5.2 用 QEMU 启动qemu-system-x86_64\-biosBuild/OvmfX64/DEBUG_GCC5/FV/OVMF.fd\-drivefiledisk.img,formatraw\-netnone\-serialstdio5.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()文件格式.efiPE32 格式模块描述.inf文件定义模块信息和依赖编译方式修改 .dsc 添加模块 →build运行方式QEMU OVMF UEFI Shell下一篇#011 UEFI Shell 是什么它能干什么——UEFI 固件里的命令行工具比你想的强大得多。 如果这篇文章对你有帮助欢迎关注本系列。300 篇 UEFI/BSP 系列文章持续更新中。