4. UEFI 固件基础:EDK2 源码结构、FDF/DSC/DEC 文件详解、UEFI 驱动模型与 Protocol 机制
好,我们进入第四章。这一章是 UEFI 固件开发的基石。很多做 ATF 的兄弟转过来看 UEFI,第一反应就是「这目录怎么这么乱?」、「这文件是干嘛的?」。别急,我当年也一样。咱们把 EDK2 的骨架摸清楚,后面联调才不至于迷路。
4.1 EDK2 源码结构:别被目录吓到
EDK2 的源码,说白了就是一个巨大的模块仓库。它不像 Linux kernel 那样有清晰的「顶层目录即子系统」的划分。EDK2 的目录,更多是按「包(Package)」来组织的。
我个人习惯,拿到一份 EDK2 代码,先看这五个核心目录:
| 目录/包名 | 作用 | 我常关注的点 |
|---|---|---|
| MdePkg | 基础定义包。UEFI 规范里的数据类型、Protocol GUID、基础库函数都在这里。 | 查 Protocol GUID 定义、基础数据结构。 |
| MdeModulePkg | 核心模块包。包含大部分 UEFI 驱动框架、核心 Protocol 的实现。 | 看 DiskIo、BlockIo 等核心驱动的实现。 |
| IntelFrameworkPkg | Intel 框架包。一些老旧的 Framework 规范实现,现在新平台用得少了。 | 兼容旧平台时才会翻它。 |
| ArmPkg / ArmPlatformPkg | ARM 架构相关包。这是咱们做 ARM 联调的重点。 | ArmGicLib、ArmMmuLib、PlatformPei 等。 |
| Platform/xxx | 具体平台包。比如 Platform/ARM/VExpressPkg。 |
板级定制代码,FDF/DSC 通常在这里。 |
Platform/xxx 下找 .dsc 和 .fdf 文件。这两个文件就是整个固件的「施工图纸」。
4.2 FDF / DSC / DEC 文件详解:固件的「三驾马车」
这三个文件,是 EDK2 构建系统的核心。你想想看,一个固件有成百上千个模块,怎么告诉编译器「我要编译哪些」、「怎么排列」、「依赖什么」?就是靠它们。
4.2.1 DEC (Package Declaration) 文件
DEC 文件定义了一个「包」对外提供的接口。包括:
- Protocols / PPIs / GUIDs:这个包里定义了哪些 Protocol 的 GUID。
- Library Classes:这个包提供了哪些库的抽象接口。
- PCDs (Platform Configuration Database):平台配置项,比如某个功能的开关、某个超时时间。
说白了,DEC 就是包的「头文件」。其他模块想用这个包里的东西,就得在 INF 文件里 !include 对应的 DEC。
// 一个典型的 DEC 文件片段 (MdePkg/MdePkg.dec)
[Guids]
gEfiSimpleTextInProtocolGuid = { 0x387477c1, 0x69c7, 0x11d2, { 0x8e, 0x39, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b } }
[Protocols]
gEfiSimpleTextInProtocolGuid ## PROTOCOL ALWAYS_PRODUCED
[PcdsFixedAtBuild]
gEfiMdePkgTokenSpaceGuid.PcdDebugPropertyMask|0x0|UINT8|0x00000001
4.2.2 DSC (Platform Description) 文件
DSC 文件定义了「要构建一个固件,需要哪些模块和库」。它有点像 Linux 内核的 defconfig。
- [Defines]:平台名称、固件版本、主芯片类型等。
- [LibraryClasses]:全局的库实例映射。比如「所有模块都用这个 DebugLib 实现」。
- [Components]:要编译的模块列表。每个模块对应一个
.inf文件。 - [Pcds]:覆盖 DEC 中定义的 PCD 默认值。
BaseMemoryLib 的实例没对上。所以,DSC 里的 LibraryClasses 段,一定要仔细核对。
4.2.3 FDF (Flash Description) 文件
FDF 文件定义了「固件在 Flash 里的布局」。它决定了哪个模块放在 Flash 的哪个地址,以及固件文件(FD)的最终二进制结构。
FDF 里最重要的两个概念:
- FD (Flash Device):整个固件镜像。
- FV (Firmware Volume):FD 里的逻辑分区。一个 FD 可以包含多个 FV。
- FFS (Firmware File System):FV 里的文件系统。每个模块(.efi 文件)被封装成一个 FFS 文件。
// FDF 文件片段示例
[FD.BL33_AP_UEFI]
BaseAddress = 0x82000000|gArmTokenSpaceGuid.PcdFdBaseAddress
Size = 0x00200000|gArmTokenSpaceGuid.PcdFdSize
ErasePolarity = 1
[FV.FVMAIN_COMPACT]
FvAlignment = 16
FvNameGuid = 0x8c8ce578-8a3d-4f1c-9935-896185c32dd3
BlockSize = 0x10000
NumBlocks = 0x20
FILE FV_IMAGE = 9E21FD93-9C72-4c15-8C4B-E77F1DB2D792 {
SECTION GUIDED EE4E5898-3914-4259-9D6E-DC7BD79403CF {
SECTION FV_IMAGE = FVMAIN
}
}
4.3 UEFI 驱动模型与 Protocol 机制
UEFI 的驱动模型,和 Linux 的设备驱动模型有相似之处,但更轻量。它的核心就是 Protocol。
4.3.1 Protocol 是什么?
Protocol 就是一个 C 语言的结构体,里面装满了函数指针。你可以把它理解成「接口契约」。比如 gEfiBlockIoProtocolGuid 对应的 EFI_BLOCK_IO_PROTOCOL,里面就有 ReadBlocks、WriteBlocks 等函数指针。
驱动 A 生产了一个 Protocol,驱动 B 消费这个 Protocol。这就是 UEFI 世界里模块间通信的唯一方式。
4.3.2 驱动模型的三步走
一个标准的 UEFI 驱动,通常遵循以下流程:
- 入口点 (Entry Point):驱动被加载时调用。在这里,驱动会安装一个
EFI_DRIVER_BINDING_PROTOCOL实例。 - Supported():UEFI 内核会遍历所有未连接的设备控制器,调用每个驱动的
Supported()函数。驱动在这里检查「这个设备我能不能驱动?」。 - Start():如果
Supported()返回成功,内核就会调用Start()。驱动在这里分配资源、安装子 Protocol(比如从 PCI 设备驱动安装BlockIo)。
Supported() 函数有没有被调用到。我经常在 Supported() 里加 DEBUG 打印,看它到底收到了什么参数。
4.3.3 一个简单的 Protocol 使用示例
假设我们要在 DXE 阶段读取硬盘数据。我们需要先找到 BlockIo Protocol:
#include <Protocol/BlockIo.h>
EFI_BLOCK_IO_PROTOCOL *gBlockIo = NULL;
EFI_STATUS Status;
// 1. 查找系统中第一个支持 BlockIo 的 handle
Status = gBS->LocateProtocol(&gEfiBlockIoProtocolGuid, NULL, (VOID**)&gBlockIo);
if (EFI_ERROR(Status)) {
DEBUG((EFI_D_ERROR, "No BlockIo protocol found! %r\n", Status));
return Status;
}
// 2. 调用 ReadBlocks 函数指针
Status = gBlockIo->ReadBlocks(gBlockIo, gBlockIo->Media->MediaId, 0, 512, Buffer);
if (EFI_ERROR(Status)) {
DEBUG((EFI_D_ERROR, "ReadBlocks failed! %r\n", Status));
}
你看,整个过程就是「找 Protocol -> 调函数」。简单直接。
4.4 本章知识体系总览
为了让你更直观地理解这三者的关系,我画了一张图:
嗯,这张图把构建时和运行时串起来了。你想想看,我们在 FDF 里排好模块,编译出 FD 镜像,烧到板子上。上电后,UEFI 内核加载这些模块,驱动们就开始通过 Protocol 互相找「对象」了。