EDK II 编译全流程实战:从环境搭建到OVMF启动
发布时间:2026/10/5 8:28:07 锦皓数字建站

2021年8月14日我花了一整天把 EDK II 的编译环境彻底跑通从拉源码、搞子模块、编 BaseTools到最后用 QEMU 把 OVMF 启动起来整个过程比预想中曲折但也比想象中过瘾。这篇文章就把那次 EDK II 编译的完整过程原原本本写下来包括环境怎么搭、参数怎么选、报错怎么排。如果你正打算入门 UEFI 固件开发或者之前跟着教程编 EDK II 一直卡在各种莫名其妙的问题上这篇内容应该能帮你少走不少弯路。1. 动手之前EDK II 编译环境的版本选型与依赖准备很多第一次接触 EDK II 的朋友上来就是git clonebuild结果卡在环境问题上。我那次也是这么过来的所以先说说版本和依赖这两件事没做好后面全是徒劳。1.1 为什么要选 stable tag而不是直接用 masterEDK II 的代码仓库是托管在 GitHub 上的 tianocore/edk2。2021年8月中旬那阵子master 分支上的提交非常频繁几乎每天都有改动包括工具链脚本、模块接口、PCD 定义都在不停调整。如果直接拿 master 来编译今天能过明天换个 commit 可能就编不过了这类问题跟你的代码没关系纯粹是基线不稳定。所以我那次直接切到了当时的稳定 tag。edk2-stable202105 是 2021 年 5 月发布的版本edk2-stable202108 要到 8 月底才正式发布8月14日那天稳定可用的就是 edk2-stable202105。切 tag 的好处很明显社区里别人遇到的问题、解决方案都是基于这个版本记录的你照着做能复现出了问题也好搜。再给第一次接触的朋友解释一句EDK II 到底是什么。简单说它是 UEFI 固件开发的标准框架由 Intel 发起、TianoCore 社区维护目前绝大多数 x86 平台的开源 UEFI 固件、驱动、引导程序都是基于它开发的。OVMF 是 EDK II 面向 QEMU/KVM 虚拟机的一个平台实现编译难度适中产物可以直接用虚拟机跑起来特别适合作为学习 EDK II 编译的入门目标。git clone https://github.com/tianocore/edk2.git cd edk2 git checkout edk2-stable202105这里有一个细节clone 的时候我没有加--recursive而是在 checkout 到稳定 tag 之后再统一更新子模块。原因后面会讲先记住这个顺序比较稳妥。1.2 编译 EDK II 需要准备的“底层零件”EDK II 的编译跟平时编译一个 Linux 内核或者 CMake 项目不太一样它除了需要常规的 C 编译器之外还依赖几个专门的小工具。我那次用的是 Ubuntu 20.04下面这套命令基本能把依赖装齐sudo apt-get install -y build-essential uuid-dev nasm iasl python3 python3-distutils逐个说下它们的作用这样你以后在别的发行版上也知道该装什么。build-essential 提供 gcc、make 这些基础编译工具这个不用多解释。uuid-dev 提供 libuuid 头文件和库EDK II 在 Linux 下编译某些工具模块时会用到缺了它会在链接阶段报找不到libuuid.a。NASM 是 x86 汇编器。EDK II 里有一些很底层的模块比如 ResetVector是用汇编写的必须靠 NASM 才能编译。缺 NASM 的话build 很可能会直接报Could not find the NASM assembler。IASL 是 ACPI 编译器用于处理 DSDT/SSDT 这类 ACPI 表。如果只是编 OVMF有些平台配置不一定会触发 ACPI 源码编译但为了保险建议装上。Python 3 是构建脚本的运行环境。这里要强调一下2021 年的时候 EDK II 已经基本完成了从 Python 2 到 Python 3 的切换。如果你在某个老旧教程里看到要求安装 Python 2那已经过时了。我一开始用的系统自带的 Python 3.8跑 edksetup.sh 和 build.py 都很正常。所有这些依赖就像做菜之前要备好的调料缺一样后面做出来的味道就不对。而且 EDK II 的报错信息有时候并不直观比如 NASM 缺失它不会告诉你“请安装 nasm”而是会在工具链检测阶段报一个比较隐晦的错误。与其到时候猜不如先装全。2. 源码拉取与 BaseTools 编译把工具链先盘活环境依赖装好之后下一步是拉源码和编译 BaseTools。很多新手在这一步容易操作顺序颠倒我得把逻辑讲清楚。2.1 子模块更新少了 openssl 后面全是坑第一次 clone EDK II 的人如果不看文档直接编大概率会挂在 CryptoPkg 上报错内容各式各样比如找不到某个 OpenSSL 头文件或者在链接阶段报一堆undefined reference。问题根源其实是子模块没有拉全。EDK II 仓库里嵌套了好几个独立的 Git 仓库最重要的就是CryptoPkg/Library/OpensslLib/openssl它指向 OpenSSL 源码的某个固定 commit。EDK II 的 HTTPS、HASH、签名验证等一堆功能都依赖 OpenSSL而 EDK II 官方没有把 OpenSSL 源码直接放进主仓库而是用 git submodule 的方式引用。我那次的操作顺序是这样cd edk2 git checkout edk2-stable202105 git submodule update --init --recursive之所以在 checkout 之后再更新子模块是因为不同 tag 对应的子模块 commit 不一样。如果你 clone 的时候就--recursive默认会把 master 对应的子模块拉下来再切到别的 tag 时子模块可能不会自动跟着变容易造成版本不一致。先切 tag 再 submodule update才能保证子模块和主仓库的版本匹配。--recursive参数是因为子模块里还可能嵌套子模块EDK II 不止 OpenSSL 一处ArmPkg 里也有子模块依赖。加了这个参数可以一次性全部拉取省得后面报错了再补。2.2 BaseTools 和 edksetup.sh 到底在干什么源码就绪之后第一件要编译的东西不是固件本身而是 BaseTools。很多初学者不理解怎么 EDK II 编个固件还得先编一套工具出来其实这套工具就是 EDK II 构建系统自身的基础设施包括build.py、GenFw、GenFv、GenSec、GenCrc32等等。打个比方EDK II 的构建过程很像是“用一套工具来加工另一套工具再用加工出来的工具去生产固件”。这些工具负责把编译出来的 ELF 或者 PE/COFF 文件做各种转换、封装成 Firmware Volume、再合成最终的.fd固件镜像。如果 BaseTools 编译有问题后面所有步骤都跑不起来。编译 BaseTools 的命令很简单make -C BaseTools -j$(nproc)这里-C BaseTools是让 make 进入 BaseTools 目录执行构建-j$(nproc)是按 CPU 核数并行编译。整个过程一般一两分钟就结束如果你看到make输出一串 gcc 命令并且在最后顺利退出说明 BaseTools 没问题。接下来是source edksetup.sh。这个脚本是 EDK II 的环境初始化入口它会做几件事设置WORKSPACE、EDK_TOOLS_PATH等环境变量把 BaseTools 的二进制目录加入PATH并且在 edk2 目录下创建Conf文件夹生成target.txt、tools_def.txt、build_rule.txt这三个初始配置文件。这里有两点要特别注意。第一每次新开一个终端窗口都要重新在 edk2 目录下执行source edksetup.sh否则环境变量丢了build命令会提示找不到。第二Conf/target.txt是默认编译配置里面定义了编译哪个平台、哪个架构、用哪套工具链是 DEBUG 还是 RELEASE。你可以修改它也可以不管它后面直接用命令行参数覆盖。我那次刚开始没搞懂这两者的关系走了不少弯路后面专门写一节讲这个。3. 核心编译实战以 OvmfPkg 为例跑通全流程环境没问题之后真正的编译试炼就开始了。我选择的第一块试验田是 OvmfPkg也就是 OVMF。3.1 target.txt 还是 build 命令行参数在Conf/target.txt里常改的配置有这么几项ACTIVE_PLATFORM OvmfPkg/OvmfPkgX64.dsc TARGET DEBUG TARGET_ARCH X64 TOOL_CHAIN_TAG GCC5ACTIVE_PLATFORM指定编译哪个平台的描述文件.dscTARGET是 DEBUG 还是 RELEASETARGET_ARCH是目标架构TOOL_CHAIN_TAG是工具链标签。我个人的建议是第一次编译直接改 target.txt 也可以但更推荐用命令行参数因为直观而且不用反复改文件build -a X64 -p OvmfPkg/OvmfPkgX64.dsc -t GCC5 -b DEBUG-a指定架构-p指定平台 DSC 文件-t指定工具链-b指定构建类型。这条命令和改 target.txt 的效果是一样的。TOOL_CHAIN_TAG这个地方有个比较迷惑人的点。GCC5 这个标签名字里带着 5但并不是说只有 GCC 5 能用。它实际上代表的是“GCC 5 及以上版本通用的工具链配置”我那次系统里装的是 GCC 9用 GCC5 标签完全没问题。EDK II 工具链命名的历史遗留问题不少CLANGPDB、VS2019、GCC5 这些标签名字只是代号别被名字误导。3.2 从 .dsc 到 .fd一个 EDK II 模块的编译流程第一次完整跑 EDK II 编译的时候我对它的内部流程其实是一头雾水的。等看多了 BUILD_LOG 才慢慢摸清楚它跟普通 C 项目编译有着本质不同。普通应用编译无非是源码、库、链接最后生成可执行文件。EDK II 要多好几道工序。先说入口。build命令首先读取.dsc文件这个文件描述一个平台包含哪些模块、哪些 PCD 配置、哪些固件卷FV。然后构建系统会根据.dsc和每个模块的.inf文件自动生成一批代码包括AutoGen.c和AutoGen.h。这里就涉及到一个很关键的机制PCD。PCD 是 EDK II 的配置项机制类似编译期可以配置的全局变量。你可以在.dsc里给某个模块设置 PCD 的值构建系统生成 AutoGen 代码时会把这些值固化到生成的 C 代码里。所以有些 PCD 的值你改了之后必须重新编译才生效不是运行期能动态修改的。接下来才是我们熟悉的编译动作。每个模块.inf文件里列出的.c文件被 gcc 编译成目标文件再链接成可执行文件。但这里生成的还不是最终的.efi文件而是一个中间格式。以 GCC 工具链为例链接出来的其实是一个 ELF 文件需要经过GenFw这个工具转换成 UEFI 标准的 PE/COFF 格式这才是真正的.efi。再往下就是 EDK II 比较独特的地方。单个.efi文件不能直接启动它需要被打包进 Firmware Volume固件卷。这个过程由GenFv完成它会把多个模块的.efi以及一些元数据、GUID、校验信息封装成一个卷类似把所有零件放进一个货架并且贴上位置标签。最后一个平台可能有多个 FV还需要把 FV 和变量区域等拼接成最终的.fd文件也就是整个固件的镜像。为了验证这个流程我特意翻过编译日志。在Build/OvmfX64/DEBUG_GCC5/目录下你能看到FV目录里放着OVMF.fd而X64目录下则是各个模块的.efi文件。我第一次看到列表里密密麻麻的.efi时才真正意识到一个 UEFI 固件里到底集成了多少个模块。3.3 编译完成后的产物验证编译结束后最重要的产物体现在Build/OvmfX64/DEBUG_GCC5/FV/OVMF.fd这个文件的大小一般是 2MB 左右也就是 0x200000 字节。看到它生成编译就算成功了但还不够我建议一定要实际跑一下只有跑起来才能确认固件内容是真的可用的。我用 QEMU 验证的命令是qemu-system-x86_64 -bios Build/OvmfX64/DEBUG_GCC5/FV/OVMF.fd -m 2048-bios指定固件镜像-m 2048给虚拟机分配 2GB 内存。运行后如果屏幕上出现 TianoCore 的 logo或者进入 UEFI Shell 界面说明整个编译链彻底通了。那一刻我心里才踏实之前所有折腾都值了。另外提一句OVMF 编译产物通常不只是OVMF.fd一个文件还会有拆分出来的OVMF_CODE.fd和OVMF_VARS.fd。前者是代码区域后者是 NVRAM 变量区域。在真实场景里把这两部分分开刷写或者挂载到虚拟机里使用会更灵活。入门阶段直接用合一的OVMF.fd就行。4. 编译进度的直观感受与提速技巧这一节不聊原理聊聊实践经验。很多人在编译 EDK II 时问得最多的问题就是怎么这么慢怎么才能快一点4.1 一次完整编译大概要多久我当时的机器是 8 核 16GB 内存首次编译 OVMFPkg差不多花了 8 分钟左右。如果机器是 4 核可能要 20 分钟以上。这里说的首次编译是指从零开始编译整个平台的所有模块涉及的模块数量非常多每个模块都要经历预处理、编译、链接、转换、打包工作量远远大于编一个普通应用。所以如果你第一次编 OVMF看到终端里不断刷屏持续好几分钟甚至十几分钟这是正常现象别以为卡死了。判断有没有在正常工作可以看编译日志的滚动它会输出正在编译的模块路径。如果长时间停在同一行不动那才要考虑是不是真的有问题。但要注意首次编译完成后再次编译会快很多因为 EDK II 构建系统支持增量编译。你只改了一个模块的代码重新运行 build它会跳过其他没变化的模块只编译受影响的部分。我后来一天之内反复改了十几次代码做实验每次编译都只要十几秒就是这个原因。4.2 让 EDK II 编译变快的几个实用招数第一并行编译参数要拉满。build 命令支持-j参数类似 make 的并行度。我常用的写法是build -a X64 -p OvmfPkg/OvmfPkgX64.dsc -t GCC5 -b DEBUG -j $(nproc)$(nproc)会自动获取 CPU 核数这样构建系统会同时启动对应数量的任务。不过这里有个小提醒内存不够大的机器并行度过高可能导致内存爆掉编译进程被系统杀掉。我试过在 4GB 内存的机器上拉满并行度直接 OOM。建议内存不大的话把-j设置为核数的一半比较稳。第二用-m参数指定单一模块编译。如果你只是在调试某一个驱动不需要每次编整个固件。build 命令支持-m参数后面接模块的.inf文件路径。比如只想编译某个驱动可以这么写build -a X64 -p OvmfPkg/OvmfPkgX64.dsc -t GCC5 -b DEBUG -m MdeModulePkg/Universal/DriverSampleDxe/DriverSampleDxe.inf这样会把该模块及其依赖编译出来速度非常快。但要注意它不会生成最终的.fd固件镜像只编译模块本身适合日常开发验证语法和链接错误。第三有条件的话把 Build 目录放到内存盘。这是一个比较高级但也非常实用的技巧。EDK II 编译过程中会产生大量小文件频繁读写磁盘如果把输出目录放到/dev/shm内存文件系统速度提升非常明显。我做过实验首次全量编译能从 8 分钟降到 4 分钟左右。做法是把Build目录做软链接指向内存盘mkdir -p /dev/shm/edk2build ln -s /dev/shm/edk2build Build不过要记住重启之后内存盘里的内容会清空所以这只适合临时用来加速不适合保存正式产物。5. 常见问题与排查技巧实录EDK II 编译报错的信息量很大但很多报错其实都指向几个常见根因。把我踩过的坑和扒过的日志整理一下按问题类别列个速查。5.1 环境类问题速查报错现象根本原因解决办法build: command not found没有执行source edksetup.sh或换终端后环境丢了在 edk2 目录下重新执行source edksetup.shCould not find the NASM assemblerNASM 未安装或版本过旧sudo apt-get install nasm并确认nasm -v能正常输出找不到 iasl / ACPI compilerIASL 未安装安装iasl包出现 Python 2 的语法报错如print语句报 SyntaxError有旧的 Python 2 脚本混入构建流程确认默认 python 是 Python 3卸载或屏蔽系统残留的 Python 2Tool chain [GCC5] was not found环境变量不全或 GCC 版本与工具链标签不匹配重新 source edksetup.sh检查gcc --version确认 GCC 5 及以上链接时报错找不到libuuiduuid-dev 未安装sudo apt-get install uuid-dev这里想多说一句build: command not found是我见过最多的初级问题。很多人下载了源码也 make 了 BaseTools但开新终端后直接敲 build系统当然找不到。EDK II 的环境是会话级的不像系统全局变量所以养成习惯每次进终端先source edksetup.sh再执行 build。5.2 EDK II 特有的编译错误实例第一类是我自己踩过的一个坑。第一次编译 OVMF 时编到 CryptoPkg 直接失败报错是找不到某个 OpenSSL 头文件。我一开始以为是系统缺包装了一堆 OpenSSL 相关的库没用。后来才发现是子模块没拉全。重新执行git submodule update --init --recursive之后这个错误就消失了。这里补一句原因EDK II 不是用系统自带的 OpenSSL而是用固定版本的 OpenSSL 子模块源码直接参与编译。所以就算你系统里装了 OpenSSL子模块缺失照样编不过两者没有关系。第二类是针对.dsc配置的错误。有时候你会看到类似PCD ... not found或者PCD ... used in ... is not declared。这种错误通俗地说就是某个模块在.inf里声明了要用某个 PCD但它在.dsc或.dec里没有定义或者没有赋初值。解决思路就是去对应.dec文件确认 PCD 声明的位置再到.dsc里补上赋值。第三类是GenFw相关的错误比如GenFw: ERROR 3000: Invalid这类错误通常是链接生成的 ELF 文件有问题或者模块用了当前工具链不支持的编译选项。我第一次遇到时完全没有头绪后来通过查看Build/OvmfX64/DEBUG_GCC5/BUILD_LOG.txt的日志才定位到具体是哪个模块的哪次链接出了问题。EDK II 的报错有个特点真正的错误往往是最早出现的那一个后面的报错基本都是连锁反应。所以排查时不要盯着最后一行看要看第一个 error 出现在哪里。排查这类问题我的通用方法论是三步走。第一步先检查环境类问题比如依赖缺失、子模块缺失这类问题占了六七成。第二步打开BUILD_LOG.txt用grep -n error过滤出所有错误行找到第一个错误点。第三步围绕第一个错误定位到具体模块再搜索这个模块的.inf和.dsc看是不是配置问题。大部分编译问题都能靠这三步解决。还有一个容易被忽略的问题如果build命令执行到一半被 CtrlC 打断了或者编译进程因为 OOM 被杀工作区里可能会留下一些不完整的中间文件。重新编译时偶尔会出现莫名其妙的错误。遇到这种情况可以先清理再重编rm -rf BuildEDK II 没有提供标准的 clean 命令直接删Build目录是最干净的清理方式。当然这会丢掉所有增量编译的缓存下次全量编译会慢一些但至少能排除脏文件的干扰。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。