UEFI - FV/FFS/FDF 的关系
目录
一、UEFI 固件卷
如果一个磁盘是没有经过分区的简单状态并且没有文件系统的话是什么样的,所有的文件扁平化的分布在整个磁盘空间,没有组织逻辑,没有文件夹等等,这不是一个理想的状态。固件卷的概念就类似于磁盘分区,它是 UEFI 固件中用于存放各种固件文件,即FFS 文件的容器。里面按规定组织了各种 UEFI 文件。比如:DXE 驱动(.efi)、PEI 模块、微码(microcode)、配置数据、ACPI 表、变量存储区、Logo 图片等资源。
在 UEFI 固件镜像中,所有模块都必须放进某个 FV 里才能被固件识别和加载。一个 FV 里都放什么内容一般按照功能决定。UEFI 固件镜像通常由多个 FV 组成,例如典型分布:
+----------------------------+
| SEC/PEI FV |
+----------------------------+
| DXE FV |
+----------------------------+
| NVRAM FV |
+----------------------------+
| Microcode FV |
+----------------------------+
不同厂商的分布不一样,但通常有两类:
-
Boot FV:存放 SEC/PEI/DXE 核心模块 → 固件启动必须依赖。
-
Runtime / Data FV:放 ACPI 表、变量存储区域、Logo、微码等资料。
那么在 EDKII 工程中 FV 文件是怎么组织和生成的呢?
先说结论,EDKII 用 FDF 文件 描述要生成哪些 FV,典型的 FDF 文件如下:
[FV.FVMAIN]
FvAlignment = 16
BLOCKSIZE = 0x10000
INF MdeModulePkg/Universal/Variable/RuntimeDxe/VariableRuntimeDxe.inf
INF MdeModulePkg/Core/Dxe/DxeMain.inf
FILE RAW {
Microcode/Microcode.bin
}
根据这个 FDF 文件,EDKII 在编译时会把.inf 描述的模块编译出的 .efi,还有.bin、.raw以及其他资源文件全部打包进 FV 中。最终生成的 FV 会被合并成 *.fd 或最终固件镜像(BIOS ROM)。
在第一章开发环境搭建和第二章 EDKII 工程目录介绍中有涉及到 QEMU 虚拟机所用的 OVMF.fd 固件的编译。EDKII 工程下的OVMF 目录如下
OvmfPkg/
├── OvmfPkgX64.dsc # 平台描述文件
├── OvmfPkgX64.fdf # 镜像布局文件
├── PlatformDxe/ # 平台初始化驱动
├── Include/
└── Library/
OvmfPkg 目录就是我们要开发的平台的固件的相关内容,如果我们有自己的板U平台,可自己创建 xxxPkg 目录,里面要放平台描述文件和镜像布局文件,有关dsc文件在第二章中有讲。fdf文件就是上文中提到的定义要生成的 FV 文件。
二、FFS 文件
平时咱们磁盘里的文件夹中能够存放各种各样的文件,txt、exe等等,但是 FV 不行,它里面只能存放 FFS 文件。FFS 可以理解为专门给固件使用的一种文件格式,它是对模块本身加上特定信息的封装,有点类似于网络中的数据报,里面放了目标数据,但是因为要遵循特定的协议,需要进行封装。具体来说,FFS 文件的内容包括
-
文件头(File Header):标识该模块是什么
-
文件体(File Body):模块本身(通常是 PE32/PE32+ 的 .efi 文件)
-
文件区块(Section):每个 Section 是一种数据类型
┌─────────────────────────┐
│ FFS File Header │ → 文件类型、GUID、校验等信息
├─────────────────────────┤
│ Section 1: PE32 │ → 模块的代码 (.efi)
├─────────────────────────┤
│ Section 2: UI Name │ → 模块名字
├─────────────────────────┤
│ Section 3: DEPS │ → 依赖信息
└─────────────────────────┘
常见 FFS 文件类型有
| 类型 | 用途 |
|---|---|
| PEI Module | PEI 阶段模块 |
| DXE Driver | DXE 驱动 |
| RAW | 原始数据,如 microcode 或 logo |
| FREEFORM | 自定义数据块 |
| PEI/DXE Combo | 既可在 PEI 又可在 DXE 使用 |
三、FDF 文件
FDF 是 EDK2 编译固件时用的 固件布局描述文件。比如 OVMF 目录下
OvmfPkg/
├── OvmfPkgX64.dsc # 平台描述文件
├── OvmfPkgX64.fdf # 镜像布局文件
├── PlatformDxe/ # 平台初始化驱动
├── Include/
└── Library/
OvmfPkgX64.fdf 就是 OVMF 固件对应的布局描述文件。它告诉编译器要生成哪些 FV,每个 FV 里放哪些 FFS 文件,最终 Firmware 镜像如何组合。如果没有 FDF,EDK2 就不知道怎么把模块打包成一个固件 ROM。它描述了ROM 大小、地址、有哪些 Firmware Volume(FV)、 各 FV 装哪些模块(INF、BIN、Raw)、 微码、ACPI、NVRAM 等放在哪、 最终如何拼成固件镜像(FD)。前面我们已经给出了一个 FDF 文件的例子,这里再举一个简单的例子来说明
[FV.FVMAIN]
INF MdeModulePkg/Core/Dxe/DxeMain.inf
INF MdeModulePkg/Universal/Variable/RuntimeDxe/VariableRuntimeDxe.inf
FILE RAW {
Microcode/Microcode.bin
}
[FV.FVMAIN]:定义一个名字叫 FVMAIN 的 Volume
INF ...:把某个 UEFI 模块(通过 INF 编译)打包进来
FILE RAW:加入一个原始文件,比如 microcode
最终这些内容会被打包成:FVMAIN.FV → 固件卷,并且会放入最终的固件镜像中。比如
~/edk2/Build/OvmfX64/DEBUG_GCC5/FV/OVMF.fd
FV 文件也会单独存在,比如
~/edk2/Build/OvmfX64/DEBUG_GCC5/FV/xxx.FV
简单描述 FV,FDS,FFS三者之间的关系:
INF 文件 → 编译 → 生成 EFI 模块 → 封装成 FFS 文件 → 打包进 FV → 合并成最终固件镜像(FD)
↑ ↑
FDF 决定怎么打包 ←--------------------+
+-------------+ +-----------+ +-----------+ +--------+
| .inf 文件 | ---> | .efi 文件 | ---> | .ffs 文件 | ---> | FV |
+-------------+ +-----------+ +-----------+ +--------+
↑
|
FDF 决定布局
上一章对 PEIM 进行了介绍,PEI 核心会扫描固件卷中的 PEIM 并逐个执行,因此再举个 PEIM 的例子:
EDK2 源码
└── DxeIplPeim.inf
└── DxeIplPeim.c
↓ 编译
DxeIplPeim.efi
↓ 封装
DxeIplPeim.ffs
↓ 打包进 FV(由 FDF 决定)
PEIFV.FV
↓ 合并多个 FV → 最终 ROM
OVMF.fd(就是固件 BIOS)
FV 文件内部包含多个 FFS 文件,如
PEIFV.FV
├── PeiMain.ffs
├── DxeIplPeim.ffs
├── LoadFilePei.ffs
├── PeiCore.ffs
└── ...
附录:模块介绍
在 EDKII 中,一个模块通常是指一个独立的、功能相对完整的软件组件,被涉及用来实现特定的 UEFI 功能,比如
- 一个驱动程序(如CPU初始化驱动、PCIe总线驱动)
- 一个UEFI应用程序(如Shell应用、引导管理器)
- 一个库的实现
- 一个协议(Protocol)的实现
每个模块都拥有自己独立的源代码目录,并且通过一个名为[name].inf的模块声明文件来描述自己。inf文件类似于这个模块的说明书,包括模块名字,源代码,依赖库,编译链接选项等。
平台和模块是上下层的关系,一个固件平台可以包括很多个功能模块,前面说了inf文件是模块描述文件,DSC/FDF就是平台描述文件。最终构建工具根据 说明书(.inf)和 总设计图(DSC/FDF)来加工构件并最终组装成房子。
一个标准的 EDKII 模块通常包含以下三个关键文件:
[name].c/.asm。模块的主要源代码文件,实现了模块的功能逻辑,包含了C语言或汇编语言的实现代码。[name].inf。这是模块的说明书。[Defines]:定义模块的基本属性,如模块类型、组件 GUID、版本等。[Sources]:列出模块所有的源代码文件(.c, .h, .asm等)。[Packages]:声明本模块所依赖的包声明文件(.dec)。这相当于C语言中的#include <...>,指明需要哪些外部接口/定义。[LibraryClasses]:声明本模块需要链接哪些库。例如UefiDriverEntryPoint,UefiLib,BaseLib等。[Protocols],[Guids],[Ppis]:声明本模块使用了哪些 EDKII 内置的或外部定义的协议、GUID 和 PPI。构建工具会据此检查依赖关系。
[name].uni。可选项。Unicode 字符串文件,用于本地化(如多语言支持)。它定义了模块中可显示的用户字符串,方便实现不同语言的用户界面。
UEFI 中模块的常见类型
| 模块类型 | 说明 | 输出结果 | 运行环境 |
|---|---|---|---|
UEFI_DRIVER |
UEFI 驱动程序 | 一个 .efi可执行文件 |
在UEFI引导服务环境(BS)下运行,通常通过 LoadImage/StartImage加载。 |
UEFI_APPLICATION |
UEFI 应用程序 | 一个 .efi可执行文件 |
与 UEFI_DRIVER类似,但通常设计为一次性执行并退出,例如在 UEFI Shell 下运行的命令。 |
DXE_DRIVER |
DXE 阶段驱动程序 | 一个 .efi可执行文件 |
在DXE(Driver Execution Environment)阶段被调度执行,是固件初始化的核心。 |
PEIM |
PEI 阶段模块 | 一个 .efi可执行文件 |
在 PEI(Pre-EFI Initialization)阶段运行,负责非常早期的硬件初始化。 |
BASE |
基础模块 | 一个库文件(如 .lib) |
这是一个特殊的类型,用于构建不依赖特定UEFI环境的库(Library)。这些库可以被其他类型的模块使用。 |
LIBRARY_CLASS |
库类实现 | 通常是一个 .lib库文件 |
这并不是一个独立的可执行模块,而是某个库类(Library Class) 的具体实现。例如,你提供一个实现了 BaseLib库类接口的实例。 |
举个简单例子
假设我们有一个非常简单的应用 HelloWorld。
MyPkg/
├── Application/
│ └── HelloWorld/
│ ├── HelloWorld.c // 源代码,打印 "Hello World"
│ ├── HelloWorld.inf // 模块声明文件
│ └── HelloWorld.uni // 可选的字符串文件
├── MyPkg.dsc // 包描述文件,需要在此声明包含HelloWorld模块
└── MyPkg.dec // 包声明文件
HelloWorld.inf文件内容可能如下:
[Defines]
INF_VERSION=0x00010005
BASE_NAME=HelloWorld
FILE_GUID=12345678-1234-1234-1234-123456789ABC
MODULE_TYPE=UEFI_APPLICATION # 声明这是一个UEFI应用
VERSION_STRING=1.0
ENTRY_POINT=UefiMain # 入口函数名
[Sources]
HelloWorld.c
[Packages]
MdePkg/MdePkg.dec # 依赖MdePkg包,因为它包含了UEFI_BASIC_SERVICES的定义
[LibraryClasses]
UefiApplicationEntryPoint # 应用需要这个库
UefiLib # 提供了Print函数等
# 如果代码中使用了gEfiSimpleTextOutProtocolGuid,则需要声明
[Protocols]
gEfiSimpleTextOutProtocolGuid
Steady Progress!

本文梳理和介绍了一下 固件卷 FV,FFS文件,FDF文件之间的组织关系,以及其在 EDKII 工程中的体现。
浙公网安备 33010602011771号