编译第一个 UEFI 程序并不轻松:环境安装耗时,链接器报错很多,.EFI 程序也不像普通桌面程序那样有成熟直观的编辑和运行体验。
这篇就按入门角度整理一下:如果只是想先编译出自己的第一个 UEFI 程序,应该从哪里开始,哪些概念要先搞清楚,哪些坑最容易遇到。
UEFI 程序是什么
UEFI 程序通常是一个 .EFI 文件。
它不是 Windows 下双击运行的普通 .exe,而是运行在 UEFI 固件环境里的 PE/COFF 可执行文件。常见场景包括:
- 启动管理器
- 硬件初始化工具
- 固件更新工具
- 引导前诊断工具
- 自定义启动流程
你在系统启动早期看到的很多功能,本质上都可能和 UEFI 应用、驱动或固件服务有关。
对初学者来说,先不用急着理解完整固件开发。第一步只要完成一个目标:编译出一个能被 UEFI Shell 或模拟器加载的 .EFI 文件。
为什么不建议一开始就上 EDK II
UEFI 正式开发经常会遇到 EDK II。
EDK II 功能完整,也更接近真实固件工程,但它对新手不太友好:
- 工程结构复杂
- 构建系统有学习成本
- 环境变量和工具链配置比较多
- 编译报错不容易看懂
- 很容易还没写代码,就先卡在环境上
如果目标只是“先跑起来一个最小 UEFI 程序”,更适合从轻量示例开始。
pbatard/uefi-simple 就是这类项目。它的定位很直接:提供一个简单的 UEFI Hello World 示例,让你先把 .EFI 编译出来。
uefi-simple 适合用来做什么
uefi-simple 适合做 UEFI 入门的第一块踏板。
它主要解决三个问题:
- 给你一个最小可编译的 UEFI 应用结构
- 帮你避开一开始就接触大型固件工程的复杂度
- 让你能先验证编译、链接、运行链路是否通
这个项目支持不同构建方式,包括 Visual Studio 2022 和 MinGW/gcc,也可以配合 QEMU 与 OVMF 做测试。
也就是说,你不一定非要准备真实机器反复重启测试。先用模拟器把程序跑起来,会安全很多。
入门前要准备哪些东西
最少需要准备几类工具。
第一类是编译工具链。
如果你在 Windows 上,可以优先考虑:
- Visual Studio 2022
- 或 MinGW/gcc
第二类是 UEFI 运行环境。
可以有两种选择:
- 用真实机器的 UEFI Shell 运行
.EFI - 用 QEMU + OVMF 在虚拟环境里测试
第三类是示例项目。
新手不建议从空目录开始手写构建脚本。直接使用 uefi-simple 这类最小示例,可以少踩很多构建系统的坑。
大致流程
一个最小 UEFI 程序的入门流程可以这样理解。
第一步,拿到示例项目。
|
|
第二步,选择构建工具链。
如果用 Visual Studio,就按项目里的 Visual Studio 方案构建。
如果用 MinGW/gcc,就按项目提供的 Makefile 或说明走。
第三步,生成 .EFI 文件。
这里最关键的是确认目标架构。常见 PC 一般是 x86_64,也就是 64 位 UEFI 环境。
第四步,把 .EFI 放到可以被 UEFI Shell 访问的位置。
如果用真实机器,通常会准备一个 FAT32 分区或 U 盘。
如果用 QEMU,可以把目录或镜像挂载进去。
第五步,在 UEFI Shell 里运行。
运行效果通常就是一个最小输出,比如打印类似 Hello World 的内容。
最容易卡住的地方
编译 UEFI 程序最容易卡的不是 C 语言本身,而是环境和链接。
常见问题包括:
- 编译器架构不对
- 目标格式不对
- 链接参数不完整
- 缺少 UEFI 入口点
- 生成的是普通可执行文件,不是 UEFI 能加载的
.EFI - QEMU 或 OVMF 没配置好
- 真实机器 Secure Boot 阻止运行未签名程序
尤其是链接器报错,经常会让新手误以为代码写错了。
实际上,很多时候是入口函数、子系统、目标架构或链接脚本配置不对。
所以第一阶段不要急着改复杂逻辑。先确保原始示例能编译、能运行,再一点点改输出内容。
测试时为什么推荐 QEMU + OVMF
真实机器测试 UEFI 程序并不是不行,但新手阶段不太方便。
因为你可能需要反复:
- 编译
- 拷贝到 U 盘
- 重启
- 进入 UEFI Shell
- 运行
- 记录报错
- 回到系统修改
这个循环很慢。
QEMU + OVMF 的好处是可以在操作系统里直接模拟 UEFI 环境。你可以更快地验证 .EFI 是否能被加载,也不容易影响真实机器启动项。
等程序基本跑通,再放到真实机器上测试,会更稳。
新手应该先改哪里
如果你已经用示例项目编译出了第一个 .EFI,下一步不要马上写复杂功能。
建议按这个顺序改:
- 先改输出文本,确认重新编译后的程序确实生效。
- 再尝试读取 UEFI 提供的简单信息。
- 再理解入口函数、输出协议和基本服务。
- 最后再考虑文件系统、图形输出、启动项管理等复杂功能。
这样做的好处是每一步都能验证。
如果一上来就改很多东西,出错时很难判断到底是代码问题、编译问题,还是运行环境问题。
和普通 C 程序有什么不同
UEFI 程序虽然可以用 C 写,但它和普通 C 程序的运行环境完全不同。
普通 C 程序通常运行在操作系统里,可以依赖标准库、文件系统、进程模型和系统调用。
UEFI 程序运行在操作系统启动之前,它依赖的是 UEFI 固件提供的服务。很多你在普通程序里习惯使用的东西,在这里并不是天然可用。
所以写 UEFI 程序时,要先适应几个变化:
- 入口函数不一样
- 输出方式不一样
- 可用库不一样
- 内存和文件访问方式不一样
- 调试方式不一样
这也是为什么推荐先从最小示例开始,而不是直接照普通 C 程序的习惯写。
一个比较现实的学习路线
如果只是入门,可以按这个路线走:
- 第一步:编译
uefi-simple - 第二步:用 QEMU + OVMF 跑起来
- 第三步:修改 Hello World 输出
- 第四步:理解 UEFI Shell 怎么加载
.EFI - 第五步:学习 UEFI 的入口函数和基本输出协议
- 第六步:再看 EDK II 或更完整的 UEFI 开发资料
这条路线的重点是先让反馈闭环跑起来。
只要你能从源码生成 .EFI,再在 UEFI 环境里看到输出,就已经跨过了最难的第一道门槛。
参考
最后一句
编译第一个 UEFI 程序,难点通常不在“写出一段 C 代码”,而在把工具链、链接格式和运行环境串起来。
先别急着做复杂功能。
从 uefi-simple 这种最小示例开始,先得到一个能运行的 .EFI,再逐步理解 UEFI 的入口、协议和构建方式,会轻松很多。