首页 / 资讯中心 / 文章详情

EFI BIOS源码实战:从SEC到BDS的启动链路与二次开发指南

EFI BIOS源码实战:从SEC到BDS的启动链路与二次开发指南 ★ FEATURED ARTICLE
简介这份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上电后第一段代码建立临时内存、切换保护模式SecMainPEIPre-EFI Initialization内存初始化、芯片组早期配置PeiCore、内存参考代码DXEDriver Execution Environment加载并调度大量驱动建立协议数据库DxeCore、各设备驱动BDSBoot Device Selection选择启动设备、加载 OS LoaderBdsDxeSEC 阶段跑在 Flash 里没有可写内存所以它只做最少的准备工作然后把控制权交给 PEI。PEI 阶段最核心的任务是“把内存点亮”内存控制器初始化代码通常和具体芯片强相关换平台时这块改动量最大。DXE 阶段是模块化程度最高的部分驱动通过协议Protocol互相发现gBSBoot Services和gRTRuntime Services两张表就是在这里建立起来的。BDS 阶段负责读启动项、走Boot####变量、最终LoadImageStartImage把 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] gEfiBlockIoProtocolGuidMODULE_TYPE决定这个模块在哪个阶段被加载ENTRY_POINT是入口函数名LibraryClasses声明它依赖哪些库实例——注意这里只写“库类名”具体用哪个实现由.dsc里的[LibraryClasses]段决定。Protocols段声明它消费或生产哪些协议 GUID。改模块时如果新增了库依赖却忘了在.inf里声明编译会报“undefined reference”这是最常见的翻车点之一。3. 把工程跑起来工具链、编译与 QEMU 验证3.1 环境准备与 BaseTools 编译这类工程的标准构建入口是edksetup.shLinux或edksetup.batWindows它负责设置WORKSPACE、EDK_TOOLS_PATH等环境变量并编译BaseTools。以 Linux 为例# 进入源码根目录 cd edk2-like # 初始化构建环境设置环境变量 source edksetup.sh # 编译 BaseTools首次必须做生成 GenFw/GenFfs 等工具 make -C BaseTools # 确认工具链配置 cat Conf/target.txtedksetup.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 兼容可以直接替换# 用编译产物作为固件启动 QEMUx86_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是否包含完整的FVFirmware 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提供gBSBoot Services和gRTRuntime 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 ( LBootOrder, 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), LBoot%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和BSBoot Services 期间有效。读写接口统一走gRT-GetVariable/gRT-SetVariable// 写入一个非易失变量 EFI_STATUS SaveMyConfig ( IN UINT8 *Config, IN UINTN ConfigSize ) { return gRT-SetVariable ( LMyConfig, 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_ACCESSOS 下的工具读不到容易误判“变量没存上”。另外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 串口日志再碰真机这个习惯救过我好几次。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站