开发者 Kevin Boone 发布了一组共 5 篇的 UEFI 裸机编程系列教程(索引页标注时间为 2026 年 11 月 3 日,站点归档列于同年 10 月,且索引里第 5 篇链接误指到了 efitest4.html,其 GitHub 仓库指向的才是正确的 efitest5.html)。这套教程面向没有 UEFI 经验的资深 Linux 程序员,演示如何不用任何操作系统,直接用一台 Linux 工作站为普通 PC 写出开机即跑的底层程序。看着像是极客周末练手的轻巧玩具,实则撕开了底层开发最容易踩空的认知盲区——你以为脱离了 Linux 内核就是直面硅片,其实一脚迈进了微软定规矩、固件当保姆的半托管领地。
先把 Boone 交付的工程卡片拆开。整套教程按顺序推进,从编译链接、EFI 系统分区、制作 DVD 或 USB 镜像,一路讲到用 Simple File Protocol 读写文件。他在 GitHub 放出了两个核心仓库:efitest 借助 Linux 下的 gnu-efi、mtools 和 xorriso,敲一行 make 就能打出可引导的 test.iso,直接塞进 VirtualBox 或烧进 U 盘启动;efitest_files 则往前推了一步,调用固件文件接口写出一个带 ls、cd、cat 命令的微型 Shell,能打开分区卷、读写文件,还能查剩余空间。对想绕开庞大内核写硬件诊断工具的 C 程序员,这套脚手架足够干净。
在 Linux 上写底层代码,为什么得守微软规矩
很多 Linux 老手第一次碰 UEFI 都会卡在一个反直觉的事实上。你在纯正的 Linux 环境里敲 GCC,产出的 .efi 镜像却必须是微软的 PE32+(PE-COFF)可执行格式,连函数怎么传参都得照搬 Microsoft x64 调用约定。
Linux 默认的 System V ABI 与 UEFI 规范水火不容。x86-64 UEFI 强制要求前 4 个整数或指针参数走 RCX、RDX、R8、R9 寄存器,返回值放 RAX;调用方必须在栈上预留 32 bytes 的 shadow space,在 call 指令前保持 16 bytes 栈对齐,还得彻底禁掉 System V 习惯使用的 red zone。当年 Intel 与微软主导 EFI 标准时把 Windows 内核习惯焊进了固件规范,车同轨、书同文,后来者哪怕满腹牢骚也得按这套规矩交过路费。
为了填平这道沟,Boone 选用的 GNU-EFI 走了一条曲线救国的路子。它先用 GCC 把 C 代码编成 Linux 原生的 ELF 中间文件,编译时硬塞进 -mno-red-zone、-DGNU_EFI_USE_MS_ABI 和 -maccumulate-outgoing-args 等参数,最后再用 objcopy 把 ELF 强行转成 PE32+ 格式。固件启动时也不是直接跳进 C 语言的 efi_main,而是先走汇编编写的 crt0-efi-x86_64.S 完成重定位与调用桥接;代码里既可以用编译器原生的 __attribute__((ms_abi))(由 GNU_EFI_USE_MS_ABI 宏展开),也能退回老式的 uefi_call_wrapper(...) 包装器。
三条工具链路线,GNU-EFI 只是最轻的跳板
把视野拉高一点看,GNU-EFI 这种先编 ELF 再转壳的做法,带着浓重的历史补丁味。今天在 Linux 主机上搞 UEFI 开发,工具链早已分化成三条清晰的代际路线。
我更看重的是工程选型里的包袱成本。真要写主板级固件或复杂硬件驱动,行业标准是官方重型框架 TianoCore EDK II——顺带提个醒,EDK II 最新上游模板已经删掉了老教程里满天飞的 -t GCC5 标签,统一改成了 GCC 或 GCCNOLTO,照抄旧博客必踩坑。而站在现代系统架构视角看,Rust 生态几乎是对 GNU-EFI 的降维打击:借助 x86_64-unknown-uefi 目标、底层的 extern "efiapi" 以及 uefi crate 的 #[entry] 宏,敲一行 cargo build --target x86_64-unknown-uefi,LLVM 就能原生直出 PE32+ 格式的 .efi 文件,彻底甩掉了 ELF 转换和汇编桥接的历史包袱。
- 建议.写几百行的硬件诊断小工具用 GNU-EFI 最省事,但凡涉及长期维护的引导器或新内核项目,直接上 Rust 的
uefi-rs生态。
没调 ExitBootServices,还不算真正的裸机
这组教程最值得掰扯的,其实是标题里 bare-metal(裸机)这个词的成色。
传统 PC 裸机开发是一场苦行:CPU 通电停在 16 位实模式,你得在狭窄的引导扇区里手搓汇编,切保护模式、开分页、跨进 64 位长模式。现代 UEFI 固件把脏活全包了,开机直接把 x64 处理器送进启用分页的 64-bit long mode,还附赠控制台、图形输出和 FAT 文件系统协议。Boone 在 efitest_files 里轻松敲出 ls 和 cat,靠的正是固件提供的启动服务(Boot Services)。
没断掉固件的奶水之前,你写的只是固件操作系统上的应用程序。
真正的生死分水岭叫 ExitBootServices()。只要没调这个函数,整台机器的内存分配与硬件控制权依然攥在固件手里,此时所谓的裸机程序,和早年寄生在 MS-DOS 中断服务上的工具没多大区别——不过是换了个驻留在主板闪存里的房东。只有先拿到当前的内存映射与 map key,再成功调用 ExitBootServices() 完成主权交割,程序才算真正接管硅片,成为独立的操作系统内核。
断奶的代价来得极快。一旦跨过 ExitBootServices(),所有启动服务瞬间蒸发:Boone 教程里用的文件读写协议、控制台文本输出,乃至图形接口 GOP 的 SetMode() 统统报废,只剩极少数运行时服务(Runtime Services)留守;想往屏幕上画点东西,只能靠断奶前提前存下来的帧缓冲区(framebuffer)地址直接往内存里怼像素。更险恶的暗礁藏在物理真机里:如果你在启动阶段用 UEFI MP Services 唤醒了多核处理器上的应用处理器(AP),必须在调用 ExitBootServices() 前把控制权老老实实交还给固件——这步在 VirtualBox 里漏掉往往相安无事,一上真实主板就会当场死机。
- 风险.切勿在虚拟机里跑通 UEFI 程序就宣称兼容物理裸机,多核控制权未归还固件会导致真机在移交硬件瞬间直接卡死。
Boone 这 5 篇小文是一块极好的敲门砖,帮 Linux 程序员省去了在 16 位汇编泥潭里打滚的苦差。但看懂了 test.iso 怎么转起来只是第一步,接下来真正值得盯紧的,是 Rust 对传统 C 引导工具链的替代速度,以及各家主板厂商在物理机固件里到底还埋了多少不守规矩的私货。
