简介:EFI BIOS源码程序是一份面向系统级与固件开发者的学习型固件源码包,覆盖UEFI规范下的启动流程、驱动加载、内存服务等核心实现,适合从事BIOS开发、嵌入式系统设计或硬件驱动调试的人群。压缩包为zip格式,共3136个文件,大小11.13MB,内含C语言源码(1315个c)、头文件(932个h)、汇编代码(193个asm)、编译配置(244个inf)及EFI可执行文件(159个efi)等,还包含DXE、SMM、UEFI Shell等模块化目录,便于按启动阶段逐一研读。目前已有535人学习使用。源码中可深入理解UEFI引导机制、EFI编程模型、硬件驱动开发流程及固件编译调试方法,包含BootManager、DriverLoader、PciLib、BaseTools、Library等典型模块,并附带Docs文档辅助学习,适合希望从源码层面掌握固件底层逻辑的开发者进阶参考。
1. EFI BIOS源码:不是读源码,是编译固件
这份EFI BIOS源码程序,解压之后和你想象的完全不同:里面没有一打开就能阅读的完整工程,而是按固件启动阶段拆开的几十个模块目录、DSC/FDF配置文件、ACPI表和一整套编译脚本。BIOS源码并不是拿来读的,是拿来改、拿来编译、再烧进Flash的。它能解决三件事:给开发板定制默认启动项和开机Logo;把UEFI驱动模块加进或移出固件;真正看懂机器重启黑屏时在哪一个阶段挂掉。适合搞嵌入式固件的人、BIOS测试工程师,以及不满足于刷别人魔改版BIOS的玩家。上手之前,先把源码骨架和启动链路搞清楚。
2. 源码目录与启动链路:五个阶段决定你先动哪个模块
一份BIOS源码解压出来,目录数量动辄上百,直接按模块名去找代码基本等于大海捞针。更靠谱的顺序是:先分层,再对阶段,最后用全局检索定位到具体文件。下面拆开讲。
2.1 一份EFI BIOS源码包里都有什么
以我经手的典型x86平台源码包来看,整个目录树可以分成四层。第一次接触源码时,把精力放在平台层和基础模块层就够了,硅层代码除非是调试硬件初始化问题,否则不建议随便动。
| 目录(常见命名) | 职责 | 动手优先级 |
|---|---|---|
| MyBoardPkg | 板级代码、DSC/FDF文件、平台PEI/DXE驱动、Logo资源 | 最高,日常改配置都在这里 |
| MdePkg | 基础EFI类型定义、Protocol/PPI接口声明、库函数 | 低,主要供查询接口定义 |
| MdeModulePkg | 通用模块:串口、PCI枚举、BDS、Setup界面、显示驱动 | 中,加驱动模块时常用 |
| SiliconInitPkg | CPU/芯片组初始化代码,一般由芯片厂商提供 | 低,只在排查早期启动问题时看 |
| BaseTools | 编译工具链封装、GUID生成、FV打包脚本 | 不需要改,但要保证完整 |
这个分层的意义在于:平台层描述“这块板子是什么”,模块层提供“这个功能怎么实现”,硅层解决“CPU和芯片组怎么点起来”。你在源码里改一个开机Logo,动的是平台层;想加一个PCIe热插拔策略驱动,大概率要去MdeModulePkg里找模板,再在平台层里接入。很多新手一上来就翻硅层代码,看两天就放弃了,其实是打开了错误的抽屉。
2.2 从SEC到TSL:按启动阶段定位入口
UEFI固件把启动过程切成几个阶段,源码仓库也基本按这个逻辑组织模块目录。固件启动时依次经过SEC、PEI、DXE、BDS、TSL几个阶段,每个阶段在源码里都有关键字可查。
| 阶段 | 做什么 | 源码里寻找的关键字 |
|---|---|---|
| SEC | 安全验证、临时RAM初始化 | ResetVector、SecCore |
| PEI | CPU/芯片组最小初始化,找内存 | PeiCore、PlatformPei |
| DXE | 加载绝大多数驱动,建立运行时服务 | DxeCore、CpuDxe、PciHostBridgeDxe |
| BDS | 枚举启动设备,进入启动菜单或引导项 | BdsDxe、UiApp、BootManagerPolicy |
| TSL | 加载OS或Shell | Shell、UefiApplication |
拿到一份陌生源码,我一般不会从头读,而是先按阶段反查。比如开机卡在PEI阶段,就去搜PlatformPei相关的目录,看是内存初始化还是芯片组寄存器配置出错。查找命令很简单:
# 在平台包里定位PEI阶段相关的目录 find MyBoardPkg -maxdepth 4 -type d | grep -i pei # 在源码里搜索PEI阶段的PPI描述符,确认某个模块属于PEI阶段 grep -r "EFI_PEI_PPI_DESCRIPTOR" --include="*.c" MyBoardPkg | head -n 20grep的用意是:只要一个模块里声明了EFI_PEI_PPI_DESCRIPTOR,它就属于PEI阶段,不会跨阶段执行。用这个方式,你可以把几十个模块快速归类,建立一张“模块-阶段”对应表。实际排查启动卡死时,配合串口日志里最后打印的模块路径,就能反推卡在哪个阶段,再回源码里找对应的.inf文件,效率比从头读到尾高得多。
2.3 DSC、DEC、FDF三件套:改配置比改代码更关键
在BIOS源码里,“改功能”很多时候不是改C代码,而是改三个配置文件。DSC决定哪些模块参与编译,DEC定义模块对外暴露的协议和可调参数,FDF决定编译出来的模块放在固件镜像的哪个位置。新手最常见的翻车就是只改了一个文件,另外两处没有同步。
| 文件 | 全称 | 作用 | 改错了会怎样 |
|---|---|---|---|
| .dsc | Platform Description | 列出编译哪些模块、设置PCD | 模块不参与编译,或PCD不生效 |
| .dec | Package Declaration | 定义包内GUID、Protocol、PCD声明 | 其他模块无法引用接口,编译报错 |
| .fdf | Flash Description | 描述FD/FV布局、各模块在镜像中的位置 | 模块编译了但没被放进固件,烧录后无此功能 |
比如想在固件里加入一个平台驱动,DSC的[Components]段至少要写一行:
[Components] MyBoardPkg/Drivers/MyPlatformDxe/MyPlatformDxe.inf这行的含义是把该驱动编译成可执行的.efi文件。但只加这一行还不够,FDF里也要在对应的固件卷中加入位置信息:
[FV.FVMAIN] INF MyBoardPkg/Drivers/MyPlatformDxe/MyPlatformDxe.inf这两处都改完,编译时才能既生成模块文件、又把它封装进固件镜像。如果只改DSC不改FDF,模块会被编译但不会出现在最终固件里,而且不会有任何报错,这是最坑的一种“静默失败”。我后来习惯每次改完驱动,都在Build输出的固件信息文件里搜一遍模块名,确认它真的被放进去了。
3. 编译环境与构建命令:把C源码变成可烧录的固件镜像
源码拿到手,最重要的一件事是能在自己的机器上编译出可以烧录的二进制。BIOS的编译环境和普通Linux用户态程序差别很大,工具链版本和辅助工具一个不对,第一步就会卡住。
3.1 搭建编译环境:工具链选型与版本边界
我倾向于在Linux下编译,Ubuntu或者Debian系的发行版最省事。依赖项看起来不少,但一条命令能装完:
# 安装EFI源码编译依赖:GCC工具链、nasm汇编器、Python、UUID库、ACPI编译工具 sudo apt install -y build-essential gcc-multilib nasm python3 \ python3-distutils uuid-dev acpica-tools逐个说明用途:build-essential提供GCC/GNU Make基础编译环境;gcc-multilib是IMPORTANT的,EFI源码的早期启动代码会以32位模式编译,只装64位GCC会在链接阶段报找不到crt1.o;nasm是汇编器,编译ResetVector等汇编模块要用;acpica-tools里的iasl负责编译ACPI表;python3-distutils服务于EDK2的构建脚本。装完后设置工作区环境变量:
export WORKSPACE=$HOME/src/MyEfi export PACKAGES_PATH=$WORKSPACE cd $WORKSPACE source edksetup.sh BaseToolssource这一步会在工作区生成Conf目录,里面是tools_def.txt等构建配置。有两点提醒:路径里不要带中文或空格,否则构建脚本会出现很难查的解析错误;另外,如果源码包自带了一套BaseTools和edksetup.sh,优先用包内的,别自己单独下载新版工具去拼,版本不匹配会导致莫名其妙的汇编错误。
3.2 执行build命令:参数与产物对照
环境初始化完成后,最核心的编译命令是build。下面是通常会执行的长这样的命令:
# 编译指定平台描述文件,输出RELEASE模式的X64固件镜像 build -p MyBoardPkg/MyBoard.dsc -b RELEASE -t GCC5 -a X64 -n 8参数含义比较固定,这里列一张速查表:
| 参数 | 作用 | 常见取值 |
|---|---|---|
| -p | 指定平台DSC文件 | MyBoardPkg/MyBoard.dsc |
| -b | 编译模式 | DEBUG / RELEASE |
| -t | 工具链标签 | GCC5,对应Conf/tools_def.txt里的定义 |
| -a | 目标架构 | IA32 / X64 |
| -n | 并行编译任务数 | 通常取CPU核心数减2 |
DEBUG和RELEASE的区别不是简单的优化开关:DEBUG模式会嵌入大量串口调试日志,模块优化级别低,固件体积更大、启动更慢,但卡死时能输出关键位置;RELEASE模式更接近生产固件,日志被裁剪,适合最终烧录。刚开始调板子,建议先用DEBUG编译一轮,把串口日志跑出来,再切RELEASE做正式镜像。编译产物在Build目录下:
# 查找编译产出的固件镜像文件 find Build/MyBoard/RELEASE_GCC5/FV -maxdepth 1 -type f -name "*.fd"输出的.fd文件就是整个固件镜像,通常对应Flash芯片的完整内容。有些平台还会额外生成.bin或.rom,本质是fd文件的不同封装或加了启动头部,烧录前先确认你的烧录器支持哪种格式。
3.3 改完平台配置重新编译:从DSC到烧录的全链路验证
现在把前面三件套的知识串起来做一个实际用例:往固件里加一个自定义USB策略驱动。先改DSC的[Components]段,追加模块路径:
[Components] MyBoardPkg/Drivers/MyUsbPolicyDxe/MyUsbPolicyDxe.inf再改FDF,把模块放进FVMAIN固件卷:
[FV.FVMAIN] INF MyBoardPkg/Drivers/MyUsbPolicyDxe/MyUsbPolicyDxe.inf然后重新编译。这种情况下建议执行一次清理编译,避免旧产物残留:
build -p MyBoardPkg/MyBoard.dsc -b RELEASE -t GCC5 -a X64 -n 8 -c加-c参数会清掉之前的目标文件,强制全量重编。编译完成后,Build目录下会生成一份固件信息文件,在里面搜索模块名,确认模块是否真的被打进固件卷:
grep -c "MyUsbPolicyDxe" Build/MyBoard/RELEASE_GCC5/FV/FVMAIN.txtgrep计数大于0,才说明这个驱动被封装进镜像。这一步是很多人的盲区:编译成功不代表烧录后功能会出现,模块必须同时满足“参与编译”和“被放置进FV”两个条件。养成编译后搜固件信息文件的习惯,能省掉大量烧录返工的折腾。
4. 避坑实录:环境、链接、布局三阶段的常见翻车
固件开发里有一半时间在跟玄学问题搏斗。这里整理几条我实际踩过、也见别人反复踩的坑,每一条都是现象、原因、解决三层讲清。
4.1 五个高频踩坑记录
现象一:source edksetup.sh时直接报错,提示ModuleNotFoundError: No module named 'distutils'。
原因:操作系统里的Python版本太新,distutils包在新版本中被移除,而源码包里的旧版构建脚本还在import它。解决:安装python3-distutils包,或者换用源码包作者常用的老版本Python。我遇到过一次后,在Ubuntu上执行apt install python3-distutils就恢复了,不建议用pip单独装setuptools绕路,容易引入其他环境问题。
现象二:编译到一半GCC报错,链接阶段找不到crt1.o,或者大量undefined reference。
原因:EFI固件的早期启动代码需要编译32位目标文件,而环境里只装了64位GCC开发库。解决:安装gcc-multilib和对应的32位libc开发包,重新编译即可。这个坑在纯64位Linux server上特别常见,装完build-essential并不代表能编32位代码。
现象三:往源码里加了新驱动目录,编译完全通过,烧进板子后功能却不存在。
原因:只把模块源码放进了目录,DSC里没有声明,或者DSC声明了但FDF里没有放置模块,模块没有被打进固件卷。解决:按验证方法做两步检查——DSC的[Components]段是否包含该.inf路径,FDF的FV段是否写入了对应INF条目。血的教训是:只看编译成功没有任何意义,要烧录前先搜固件信息文件。
现象四:改了一个PCD参数,编译烧录后进系统发现还是旧值。
原因:PCD的默认值通常在DEC文件里定义,但平台DSC会覆盖默认值,DEC里的修改根本不会生效;如果同一个PCD在多个DSC/FDF里重复赋值,构建系统可能取到你意料之外的值。解决:全局搜索这个PCD标识符,把所有出现的赋值点都过一遍,在平台DSC的统一PCD段修改,并删掉其他冗余赋值。改PCD之前先搜索引用,这是节省一下午调试时间的习惯。
现象五:编译出的固件烧进板子,主板无显示、复位循环。
原因:FD文件设计容量超过Flash芯片容量,或者FDF里定义的Flash基地址与板卡实际映射不一致,导致PEI早期阶段崩溃。解决:先核对FDF里Flash布局的定义值,确认FD大小和基地址与硬件对应;再用DEBUG模式重新编译,打开串口重定向,看日志最后停在哪一行。日志指向的模块路径,就是崩溃位置最快的线索。
4.2 烧录前的模拟验证:别拿主板当第一实验场
并不是每次改完都要立刻上编程器。平台包里往往自带模拟验证脚本,或者你可以找x86虚拟机环境加载编译出的FD文件。这类验证能覆盖DXE和BDS阶段的基本行为,比如启动菜单能否出现、驱动是否加载、Shell能否进入。
# 通过平台包自带的模拟验证脚本,加载刚编译出来的FD镜像 ./run_sim.sh -B Build/MyBoard/DEBUG_GCC5/FV/MyBoard.fd脚本里-B参数指定固件镜像路径,加载后观察串口输出和图形界面。在模拟环境里能进启动菜单、能引导到Shell,至少说明DXE/BDS阶段没有系统性崩溃。但要注意边界:模拟环境不能验证Flash布局时序、硬件寄存器写入和实际PCI设备枚举,ASL表里依赖真实硬件的部分也测不到。所以我的习惯是:改Logo、改默认启动项这类非硬件相关配置,先走模拟;改芯片组初始化、内存时序、Flash布局,直接放弃模拟,上串口调试器在真机上排。
5. 进阶定制:固化开机Logo与默认BIOS设置
编译通过只是起点,多数人下载这套源码是想做定制。这里讲两个高频需求的实际做法。
5.1 替换开机Logo
UEFI固件的开机Logo一般不是写在代码里,而是作为固件卷里的一个二进制资源文件,由Logo驱动在BDS阶段读取并显示。替换流程分三步:准备一张符合平台分辨率要求的BMP图片,放在平台包的Logo目录下;在FDF文件里把Logo资源的文件路径指过去;重新编译并验证。FDF里通常有这样的描述:
[FV.FVMAIN] FILE DATA = $(LOGO_FILE_GUID) { RAW MyBoardPkg/Logo/DefaultLogo.bmp }这里的FILE DATA块把一个BMP文件作为原始数据封装进固件卷,Logo驱动通过GUID找到它。换Logo时只要替换BMP文件,其他都不用动。注意图片格式和尺寸要符合平台限制,常见的是BMP格式、分辨率不超过屏幕原生分辨率;格式不对时显示会花屏或直接黑屏,但启动流程不受影响。
5.2 固化默认BIOS设置
想让自己定制的默认设置一刷就生效,简单做法是修改平台包里的默认值模块。很多平台源码会带一个Setup默认值的数据文件或C数组,开机时由BDS阶段把这份默认表写入NVRAM。搜索关键字一般是SetupDefault、DefaultData、CfgDefault。找到之后,把目标设置项的默认值改掉,重新编译刷入,就能做到“刷完开机即是你的配置”。这个环节最容易翻车的是字节偏移对不上,所以改之前务必先导出原厂固件的默认值做对比。
有次我改默认启动项,为图快直接改了源码里的默认NVRAM变量数组,结果刷完机器起不来,又花了大半天才用编程器把出厂镜像救回来。从那以后,每次改BIOS源码都强制走一遍:全局搜索符号、DSC声明、FDF放置、模拟验证、最后才碰烧录器。希望帮到你。
本文还有配套的精品资源,点击获取