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 默认值。
我曾经踩过的坑: 在 DSC 里漏加了某个 LibraryClass 的映射,结果编译出来的固件在运行到某个驱动时直接挂掉。查了两天,才发现是 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
    }
}
核心逻辑: DEC 定义「有什么」,DSC 定义「用什么」,FDF 定义「怎么放」。这三者缺一不可。

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 驱动,通常遵循以下流程:

  1. 入口点 (Entry Point):驱动被加载时调用。在这里,驱动会安装一个 EFI_DRIVER_BINDING_PROTOCOL 实例。
  2. Supported():UEFI 内核会遍历所有未连接的设备控制器,调用每个驱动的 Supported() 函数。驱动在这里检查「这个设备我能不能驱动?」。
  3. 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 本章知识体系总览

为了让你更直观地理解这三者的关系,我画了一张图:

EDK2 构建与驱动模型核心关系图 EDK2 构建与驱动模型核心关系 DEC 文件 定义接口与PCD DSC 文件 选择模块与库 FDF 文件 定义Flash布局 固件镜像 (FD) 包含多个FV和FFS UEFI 驱动 安装/消费Protocol Protocol 机制 接口契约与通信 设备控制器 Supported/Start 核心逻辑: 1. DEC/DSC/FDF 三者在构建时确定固件内容与布局。 2. 运行时,UEFI 驱动通过 Protocol 机制实现设备枚举与功能调用。

嗯,这张图把构建时和运行时串起来了。你想想看,我们在 FDF 里排好模块,编译出 FD 镜像,烧到板子上。上电后,UEFI 内核加载这些模块,驱动们就开始通过 Protocol 互相找「对象」了。

总结一下: 搞懂 EDK2 的源码结构,你就能快速定位代码;搞懂 FDF/DSC/DEC,你就能控制固件构建;搞懂 Protocol 机制,你就能理解 UEFI 世界里的一切交互。这三板斧,是 UEFI 联调的基础。