☰
EFI BIOS源码实战:从SEC到BDS的启动链路与二次开发指南
2026/11/4 10:23:45 网站建设 项目流程

简介:这份EFI BIOS源码程序面向系统级编程、固件开发与嵌入式设计人员,提供从启动初始化到硬件驱动的完整固件实现参考,适合具备一定C语言与底层硬件基础的中高级开发者研读。压缩包共3136个文件,约11.13MB,以1315个C源文件和932个头文件为主体,辅以244个INF模块描述、193个汇编文件及159个EFI可执行文件,另有DXS、Makefile、UNI、VFR等构建与界面配置资源,覆盖BaseTools、Library、Driver、Platform、Dxe、Smm、Uefi、Docs等模块。已有538人学习下载。读者可从中理解UEFI规范下的引导流程与内存模型,掌握EFI应用与驱动编写方法,学习PCI、AHCI等硬件驱动实现,并借助文档与构建脚本走通从源码编译到固件生成、签名验证的完整流程,为固件调试与实际问题排查积累可复用的思路。

1. 拿到一份 EFI BIOS 源码,先搞清楚它到底能干什么

很多人第一次接触 BIOS 源码,脑子里想的是“刷个机、改个开机画面”,结果解压一看,几万个文件、一堆.c.h.asm.inf.dsc,连入口在哪都找不到。这份 EFI BIOS 源码程序,本质是一套基于 UEFI 规范的固件工程骨架:从 SEC 阶段上电、PEI 阶段内存初始化、DXE 阶段驱动调度,到 BDS 阶段引导操作系统,整条链路都在里面。它适合两类人:一是想搞懂“电脑按下电源键到进系统之间发生了什么”的固件方向开发者,二是做嵌入式、国产平台适配、需要定制启动流程的工程师。你不需要先成为汇编高手,但得愿意读 C 代码、看得懂 Makefile 和构建脚本,否则后面每一步都会卡住。

2. 工程结构拆解:从 SEC 到 BDS 的目录与模块映射

2.1 顶层目录长什么样

拿到源码包,先别急着编译,花十分钟把目录扫一遍,比后面翻车再回头找强得多。这类 EFI BIOS 工程通常按 UEFI 规范的阶段划分目录,常见结构如下:

edk2-like/ ├── MdePkg/ # 基础库:数据类型、协议定义、通用库函数 ├── MdeModulePkg/ # 通用模块:DXE Core、BDS、变量服务等 ├── UefiCpuPkg/ # CPU 相关:SEC/PEI 阶段的 CPU 初始化 ├── ShellPkg/ # UEFI Shell 实现 ├── Platform/ # 平台相关:具体板子的 DSC/FDF/INF 配置 │ └── YourBoardPkg/ │ ├── YourBoard.dsc # 平台描述文件,决定编译哪些模块 │ ├── YourBoard.fdf # 固件布局文件,决定最终镜像怎么排 │ └── Include/ # 平台头文件 └── BaseTools/ # 构建工具链:GenFw、GenFfs 等

MdePkg和MdeModulePkg是“公共底座”,一般不动;真正要改的是Platform下的板级包。.dsc文件决定“编译哪些模块、用什么库实例”,.fdf决定“这些模块在 Flash 里怎么摆放”。新手最容易犯的错是直接改公共包,结果换个平台全崩,血泪经验就是:平台差异一律往Platform/里收。

2.2 启动链路的四个阶段

UEFI 固件的启动不是一条直线,而是四个阶段接力:

阶段全称主要职责典型模块
SECSecurity Phase上电后第一段代码,建立临时内存、切换保护模式SecMain
PEIPre-EFI Initialization内存初始化、芯片组早期配置PeiCore、内存参考代码
DXEDriver Execution Environment加载并调度大量驱动,建立协议数据库DxeCore、各设备驱动
BDSBoot Device Selection选择启动设备、加载 OS LoaderBdsDxe

SEC 阶段跑在 Flash 里,没有可写内存,所以它只做最少的准备工作,然后把控制权交给 PEI。PEI 阶段最核心的任务是“把内存点亮”,内存控制器初始化代码通常和具体芯片强相关,换平台时这块改动量最大。DXE 阶段是模块化程度最高的部分,驱动通过协议(Protocol)互相发现,gBS(Boot Services)和gRT(Runtime Services)两张表就是在这里建立起来的。BDS 阶段负责读启动项、走Boot####变量、最终LoadImage+StartImage把 OS Loader 拉起来。

2.3 一个模块的 INF 文件怎么读

每个驱动模块都有一个.inf描述文件,它是模块的“身份证”。看一个典型例子:

[Defines] INF_VERSION = 0x00010005 BASE_NAME = MyDriver FILE_GUID = 12345678-1234-1234-1234-123456789abc MODULE_TYPE = DXE_DRIVER ENTRY_POINT = MyDriverEntry [Sources] MyDriver.c [Packages] MdePkg/MdePkg.dec MdeModulePkg/MdeModulePkg.dec [LibraryClasses] UefiDriverEntryPoint UefiLib [Protocols] gEfiBlockIoProtocolGuid

MODULE_TYPE决定这个模块在哪个阶段被加载,ENTRY_POINT是入口函数名,LibraryClasses声明它依赖哪些库实例——注意这里只写“库类名”,具体用哪个实现由.dsc里的[LibraryClasses]段决定。Protocols段声明它消费或生产哪些协议 GUID。改模块时,如果新增了库依赖却忘了在.inf里声明,编译会报“undefined reference”,这是最常见的翻车点之一。

3. 把工程跑起来:工具链、编译与 QEMU 验证

3.1 环境准备与 BaseTools 编译

这类工程的标准构建入口是edksetup.sh(Linux)或edksetup.bat(Windows),它负责设置WORKSPACE、EDK_TOOLS_PATH等环境变量,并编译BaseTools。以 Linux 为例:

# 进入源码根目录 cd edk2-like # 初始化构建环境,设置环境变量 source edksetup.sh # 编译 BaseTools(首次必须做,生成 GenFw/GenFfs 等工具) make -C BaseTools # 确认工具链配置 cat Conf/target.txt

edksetup.sh干的事不复杂:把BaseTools/Source/C/bin加进PATH,导出WORKSPACE指向当前目录。make -C BaseTools编译出来的是构建期工具,包括把 PE/COFF 转成 TE 格式的GenFw、把模块打包成 FFS 文件的GenFfs。如果这一步报错,九成是缺gcc、make、python3或uuid-dev这类基础依赖,先把系统包补齐再重来。

3.2 用 DSC/FDF 指定平台并编译

环境就绪后,编译命令通过build工具驱动,关键参数是-p(平台 DSC)、-a(架构)、-t(工具链标签):

# 编译 64 位 X64 平台固件,使用 GCC5 工具链 build -p Platform/YourBoardPkg/YourBoard.dsc \ -a X64 \ -t GCC5 \ -b DEBUG # 编译产物位置 ls Build/YourBoard/DEBUG_GCC5/X64/

-b DEBUG会打开DEBUG()/ASSERT()宏输出,方便定位问题;发布时换成-b RELEASE可以减小体积、去掉调试串口输出。编译成功后,Build/目录下会生成.fd固件镜像和.efi模块文件。如果报“module not found”,检查.dsc的[Components]段是否包含该模块路径;如果报“FDF parse error”,检查.fdf里的INF规则和FD区域大小是否匹配。

3.3 QEMU 里跑一遍,别急着上真机

真机刷写有风险,先用 QEMU 验证启动链路是稳妥做法。OVMF 是 QEMU 常用的开源 UEFI 固件,如果你编译出的.fd结构和 OVMF 兼容,可以直接替换:

# 用编译产物作为固件启动 QEMU(x86_64) qemu-system-x86_64 \ -bios Build/YourBoard/DEBUG_GCC5/FV/YOURBOARD.fd \ -m 1024 \ -serial stdio \ -net none

-serial stdio把串口输出重定向到终端,DEBUG 版本的日志会从这里打出来,能看到 SEC、PEI、DXE 各阶段的进度。-net none避免网络设备初始化干扰。如果 QEMU 直接退出或黑屏无输出,先确认.fd是否包含完整的FV(Firmware Volume),再看串口有没有打印“SEC”字样——没有的话,问题多半在 SEC 入口或复位向量配置上。

提示:QEMU 验证通过不代表真机能跑,内存时序、SPI Flash 布局、芯片组初始化这些差异只有真机才暴露。上真机前务必备份原厂固件,用编程器留一条后路。

4. 核心代码拆解:入口函数、协议安装与 BDS 引导

4.1 DXE 驱动的入口与协议安装

DXE 驱动最典型的写法是“入口函数里安装协议,然后返回”。看一个简化版块设备驱动:

#include <Uefi.h> #include <Library/UefiBootServicesTableLib.h> #include <Library/UefiLib.h> #include <Protocol/BlockIo.h> // 驱动入口:由 DXE Core 在调度时调用 EFI_STATUS EFIAPI MyDriverEntry ( IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable ) { EFI_STATUS Status; EFI_HANDLE Handle = NULL; // 安装 BlockIo 协议到新句柄上 Status = gBS->InstallMultipleProtocolInterfaces ( &Handle, &gEfiBlockIoProtocolGuid, &mBlockIo, NULL ); if (EFI_ERROR (Status)) { DEBUG ((DEBUG_ERROR, "Install BlockIo failed: %r\n", Status)); return Status; } return EFI_SUCCESS; }

ImageHandle是当前驱动镜像的句柄,SystemTable提供gBS(Boot Services)和gRT(Runtime Services)入口。InstallMultipleProtocolInterfaces是 DXE 阶段的核心动作:把协议 GUID 和实现结构体绑定到一个句柄上,其他驱动通过LocateProtocol或OpenProtocol就能发现它。注意mBlockIo这个结构体里的函数指针必须全部实现,少一个就会在调用时跳到空地址,表现为“随机死机”,排查起来非常痛苦。

4.2 BDS 阶段怎么选启动项

BDS 的核心逻辑是读BootOrder变量,按顺序尝试Boot####启动项。简化后的流程:

// 遍历 BootOrder,找到第一个可启动项 EFI_STATUS BootFromOrder ( VOID ) { UINT16 *BootOrder; UINTN OrderSize; UINTN Index; EFI_STATUS Status; // 读取全局变量 BootOrder Status = gRT->GetVariable ( L"BootOrder", &gEfiGlobalVariableGuid, NULL, &OrderSize, BootOrder ); if (EFI_ERROR (Status)) { return Status; } for (Index = 0; Index < OrderSize / sizeof (UINT16); Index++) { // 构造 Boot0001、Boot0002 等变量名 CHAR16 BootVarName[9]; UnicodeSPrint (BootVarName, sizeof (BootVarName), L"Boot%04x", BootOrder[Index]); // 读取启动项并尝试加载 Status = TryBootOption (BootVarName); if (!EFI_ERROR (Status)) { return EFI_SUCCESS; } } return EFI_NOT_FOUND; }

BootOrder是一个UINT16数组,每个元素对应一个Boot####变量的编号。TryBootOption内部会解析EFI_LOAD_OPTION结构,拿到设备路径和可执行文件路径,然后调LoadImage和StartImage。改启动逻辑时,常见需求是“插入一个自定义启动项”或“跳过某个设备”,前者改BootOrder写入顺序,后者在TryBootOption里加设备路径过滤。注意GetVariable的DataSize要先传缓冲区大小,传 0 会返回EFI_BUFFER_TOO_SMALL,这是 UEFI 变量服务的标准套路。

4.3 变量服务的读写与持久化

UEFI 变量分两类:NV(非易失,存 Flash)和BS(Boot Services 期间有效)。读写接口统一走gRT->GetVariable/gRT->SetVariable:

// 写入一个非易失变量 EFI_STATUS SaveMyConfig ( IN UINT8 *Config, IN UINTN ConfigSize ) { return gRT->SetVariable ( L"MyConfig", &gMyConfigGuid, EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS, ConfigSize, Config ); }

属性位决定变量的生命周期:NON_VOLATILE表示掉电保留,BOOTSERVICE_ACCESS表示 BDS 之前可访问,RUNTIME_ACCESS表示进 OS 后仍可读写。三个属性通常一起用。如果只写NON_VOLATILE不写RUNTIME_ACCESS,OS 下的工具读不到,容易误判“变量没存上”。另外,Flash 变量区有容量上限,频繁SetVariable会触发垃圾回收,极端情况下写失败返回EFI_OUT_OF_RESOURCES,调试时看到这个错误码先查变量区剩余空间。

5. 避坑与排查:编译、启动、刷写三个环节的常见问题

5.1 编译报“undefined reference”但库明明加了

现象:.inf里写了LibraryClasses,.dsc里也配了库实例,编译仍报某个函数未定义。原因通常是库类名拼写和.dec里声明的不一致,或者.dsc的[LibraryClasses]段作用域不对——平台段和模块段是分开的,模块段没继承到。解决:先用grep在.dec里确认库类名精确拼写,再检查.dsc里该模块所在[Components]段上方是否有对应的[LibraryClasses]覆盖。我一般会在.dsc顶部统一配一份全局库实例,模块级只做例外覆盖,减少这类问题。

5.2 QEMU 启动黑屏、串口无输出

现象:qemu-system-x86_64 -bios xxx.fd执行后窗口黑屏,终端也没有 SEC 日志。原因多半是.fd不是完整的固件卷,或者复位向量没指向 SEC 入口。解决:用GenFw或固件分析工具确认.fd里包含FV签名,检查.fdf的[FD]段BaseAddress和Size是否与 QEMU 期望的 Flash 布局一致。另一个常见原因是串口初始化代码没跑,DEBUG 输出被吞掉——临时在 SEC 入口加一句直接写0x3F8端口的汇编,能快速判断代码到底有没有执行到。

5.3 真机刷写后无法开机

现象:刷入自编译固件后,机器风扇转但无显示,或反复重启。原因通常是 SPI Flash 布局和原厂不一致,或者 ME/EC 区域被覆盖。解决:刷写前用编程器完整备份原厂固件,确认.fdf里各FD区域的偏移和大小与原厂一致,尤其是描述符区(Descriptor)和 ME 区绝对不能动。血泪经验:第一次上真机一定用外部编程器 + 夹子,不要用软件刷写工具,变砖后还能救回来。

5.4 变量写入失败返回 EFI_OUT_OF_RESOURCES

现象:SetVariable偶尔失败,重启后变量丢失。原因是非易失变量区容量耗尽,垃圾回收没及时触发,或者单次写入的数据超过单变量上限。解决:减少不必要的变量写入,合并配置项;检查变量区大小配置(通常在.dsc的PcdFlashNvStorageVariableSize),必要时扩大。调试阶段可以先用BOOTSERVICE_ACCESS不带NON_VOLATILE验证逻辑,确认无误再开持久化。

5.5 新增驱动不执行,日志里也没有它的名字

现象:.dsc的[Components]加了模块,编译通过,但启动日志里找不到该驱动的DEBUG输出。原因通常是.fdf里没有把该模块的INF规则加进对应的FV区域,模块编译出来了但没被打进固件。解决:检查.fdf的[FV.xxx]段是否有INF Platform/YourBoardPkg/MyDriver/MyDriver.inf,并确认该FV被[FD]引用。另一个可能是模块MODULE_TYPE与所在FV的FvNameGuid调度规则不匹配,DXE 调度器不会加载类型不符的模块。

6. 二次开发改造:从改一个启动项到加一个自定义协议

6.1 改造思路:先定位“最小改动面”

拿到这套源码做二次开发,最忌讳上来就大改。我的习惯是先画一张“改动影响图”:要改的功能落在哪个阶段、涉及哪些模块、这些模块被哪些.dsc/.fdf引用。比如“开机显示自定义 Logo”,改动面在 BDS 阶段的BootLogo相关模块和.fdf里的图片资源;比如“增加一个工厂诊断模式”,改动面在 DXE 新增一个驱动 + BDS 里加一个启动项判断。改动面越小,验证成本越低。

6.2 加一个自定义 DXE 协议并让 Shell 能调用

假设要给产线加一个“读硬件版本号”的协议,步骤是:定义 GUID 和接口结构体、写驱动安装协议、写一个 UEFI Shell 应用调用它。先定义接口:

// MyHwInfo.h #define MY_HW_INFO_PROTOCOL_GUID \ { 0xa1b2c3d4, 0xe5f6, 0x7890, \ { 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0 } } typedef struct _MY_HW_INFO_PROTOCOL { UINT32 HwVersion; UINT32 (*GetHwVersion) (VOID); } MY_HW_INFO_PROTOCOL; extern EFI_GUID gMyHwInfoProtocolGuid;

驱动入口里InstallMultipleProtocolInterfaces安装它,Shell 应用里LocateProtocol拿到指针后调用GetHwVersion。验证方法:编译后进 UEFI Shell,执行你的应用,看输出是否和预期一致;再用dh -p MyHwInfo命令确认协议已挂在句柄上。这套流程跑通一次,后面加任何自定义协议都是复制粘贴改 GUID。

6.3 验证清单与回滚策略

每次改动后,我固定走一遍验证清单:QEMU 启动到 Shell 无异常 → 串口日志无ASSERT→ 目标功能输出正确 → 原有启动项仍能进 OS。真机阶段再加一条:刷写后连续冷启动三次,确认变量持久化正常。回滚策略是保留上一版.fd和原厂备份,任何一步失败立即回退,不在故障固件上继续叠加改动。

注意:改.fdf布局时,先确认 Flash 描述符区的只读属性,误改描述符会导致整片 Flash 无法识别,恢复只能靠编程器。

从那以后我每次动 BIOS 源码,都强制先跑一遍 QEMU + 串口日志,再碰真机,这个习惯救过我好几次。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询