第3章:UEFI固件架构——固件文件系统(FFS)、固件卷(FV)、固件文件与协议
好,我们进入第三章。这一章要聊的是UEFI固件的“骨架”——固件文件系统(FFS)、固件卷(FV)、固件文件,以及操作它们的协议。说白了,就是BIOS芯片里那点东西是怎么组织、怎么存放、怎么被读出来的。
我记得刚入行那会儿,第一次用工具dump出BIOS ROM,打开一看全是二进制数据,完全摸不着头脑。后来才知道,这些数据不是乱堆的,它们遵循一套严格的“文件系统”规范。嗯,今天我们就把它拆开看看。
3.1 固件卷(FV)——固件的“分区”
固件卷,英文叫Firmware Volume,简称FV。你可以把它想象成硬盘上的一个分区。整个BIOS ROM可以被划分成多个FV,每个FV都有自己的用途。
举个例子,一个典型的UEFI BIOS ROM里,通常会有:
- 主BIOS区域:存放核心的DXE驱动、BDS(启动设备选择)逻辑。
- 恢复区域:一个备份的、最小的固件,用于主区域损坏时恢复。
- NVRAM区域:存放用户设置、启动顺序等非易失性数据。
- 微代码补丁区域:CPU微码更新。
每个FV都有一个头部(FV Header),里面记录了它的大小、校验和、属性(比如是否可写、是否包含EFI头等)。
关键点:FV是固件存储的顶层容器。所有固件文件都存放在FV里。
3.2 固件文件(Firmware File)——固件的“文件”
FV里面装的是什么呢?是固件文件(Firmware File)。每个固件文件都有一个唯一的GUID来标识它的类型和用途。比如:
DXE Core的GUID是固定的。PEI Core的GUID也是固定的。- 各种驱动、协议、数据块都有自己的GUID。
固件文件的结构很简单:
+-----------------------+
| Firmware File Header | <-- 包含GUID、类型、属性、大小
+-----------------------+
| Firmware File Section | <-- 实际数据,可以有多个Section
| (Section 1) |
| (Section 2) |
| ... |
+-----------------------+
每个文件可以包含多个Section(节)。Section是更细粒度的数据单元。常见的Section类型有:
| Section类型 | 描述 | 我见过的坑 |
|---|---|---|
EFI_SECTION_PE32 |
存放PE32+格式的可执行代码(驱动、应用) | 对齐问题导致加载失败,我排查了一整天 |
EFI_SECTION_COMPRESSION |
压缩的数据块,解压后才能看到内部Section | 解压算法选错,直接死机 |
EFI_SECTION_GUID_DEFINED |
自定义格式,由GUID指定的解析器处理 | 供应商自定义格式,不通用 |
EFI_SECTION_RAW |
原始二进制数据,比如微码、Logo图片 | 大小端问题,数据读出来是反的 |
EFI_SECTION_FREEFORM_SUBTYPE_GUID |
自由格式,子类型由GUID指定 | 很少用,但遇到时很头疼 |
我的习惯:在调试时,我经常用工具(比如UEFITool)把BIOS ROM拆开,看看每个FV里到底有哪些文件、哪些Section。这比看代码直观多了。
3.3 固件文件系统(FFS)——文件系统的“元数据”
FFS(Firmware File System)不是我们通常理解的文件系统(比如FAT32、NTFS)。它更像是一套管理固件文件的规则和数据结构。FFS定义了:
- 文件如何对齐(通常是8字节对齐)。
- 文件之间如何填充(用0xFF填充空闲空间)。
- 文件头部的格式(GUID、类型、属性、大小、校验和)。
- 如何遍历FV里的所有文件。
FFS的布局非常紧凑,没有目录树,没有文件名(只有GUID)。你想想看,BIOS芯片就那么点大(16MB、32MB),每一字节都很宝贵,所以设计上追求极致的空间效率。
我曾经踩过的坑:有一次我手动修改了BIOS ROM里的一个文件,但忘记更新文件头部的校验和。结果刷进去后,主板直接变砖。后来我养成了习惯:修改任何固件文件后,必须用工具重新计算并更新校验和。
3.4 固件文件系统协议(Firmware File System Protocol)
好了,前面说的都是固件在ROM里的静态布局。那在运行时,UEFI固件怎么访问这些文件呢?答案是:通过协议。
EFI_FIRMWARE_FILE_SYSTEM_PROTOCOL 是UEFI规范定义的一个关键协议。它提供了在运行时读取FV中固件文件的能力。这个协议的核心接口是:
typedef struct _EFI_FIRMWARE_FILE_SYSTEM_PROTOCOL {
UINT64 Revision;
EFI_FFS_OPEN_VOLUME OpenVolume;
// 注意:规范里其实只有OpenVolume一个函数
// 但实际实现中,通常会通过VolumeHandle来操作
} EFI_FIRMWARE_FILE_SYSTEM_PROTOCOL;
等等,你可能会问:“只有一个函数?那怎么读文件?”
嗯,这里有个小细节。这个协议的作用是“打开”一个FV,返回一个Volume Handle。然后,你通过这个Volume Handle去调用其他协议(比如EFI_FILE_PROTOCOL)来遍历和读取文件。说白了,它就是一个“入口”。
实际使用中,更常见的是通过EFI_FILE_PROTOCOL来操作。UEFI固件会把FV挂载成一个“文件系统”,你可以用EFI_FILE_PROTOCOL的Open()、Read()、Close()等函数来读写文件。这跟操作普通文件系统几乎一样。
核心思路:UEFI把固件卷抽象成了一个文件系统。驱动开发者可以通过标准的文件操作接口来访问固件里的任何文件(驱动、数据、配置)。
3.5 整体架构图
为了让你更直观地理解,我画了一张图。这张图展示了从BIOS ROM到固件文件Section的完整层级关系。
3.6 实战:如何遍历FV中的文件
说了这么多理论,我们来点实际的。假设你正在写一个UEFI驱动,需要遍历某个FV里的所有固件文件,找到特定GUID的驱动。代码大概长这样:
EFI_STATUS
EnumerateFilesInFv (
IN EFI_HANDLE FvHandle
)
{
EFI_STATUS Status;
EFI_FIRMWARE_FILE_SYSTEM_PROTOCOL *FfsProtocol;
EFI_FILE_PROTOCOL *VolumeFile;
EFI_FILE_PROTOCOL *CurrentFile;
EFI_FILE_INFO *FileInfo;
UINTN BufferSize;
CHAR16 FileName[256];
// 1. 从Handle上获取FFS协议
Status = gBS->HandleProtocol (
FvHandle,
&gEfiFirmwareFileSystemProtocolGuid,
(VOID **)&FfsProtocol
);
if (EFI_ERROR (Status)) {
return Status;
}
// 2. 打开FV,获取Volume File Handle
Status = FfsProtocol->OpenVolume (FfsProtocol, &VolumeFile);
if (EFI_ERROR (Status)) {
return Status;
}
// 3. 遍历Volume里的文件
// 先打开根目录
Status = VolumeFile->Open (VolumeFile, &CurrentFile, L"\\", EFI_FILE_MODE_READ, 0);
if (EFI_ERROR (Status)) {
return Status;
}
// 4. 循环读取目录项
BufferSize = sizeof (EFI_FILE_INFO) + 256;
FileInfo = (EFI_FILE_INFO *) AllocatePool (BufferSize);
while (TRUE) {
Status = CurrentFile->Read (CurrentFile, &BufferSize, FileInfo);
if (EFI_ERROR (Status) || BufferSize == 0) {
break; // 读完或出错
}
// 5. 检查文件名(GUID字符串)
// 比如:12345678-1234-1234-1234-123456789abc
if (StrCmp (FileInfo->FileName, L"12345678-1234-1234-1234-123456789abc") == 0) {
// 找到了!可以读取文件内容
// ...
break;
}
// 继续下一个文件
CurrentFile->Close (CurrentFile);
Status = VolumeFile->Open (VolumeFile, &CurrentFile, FileInfo->FileName, EFI_FILE_MODE_READ, 0);
if (EFI_ERROR (Status)) {
break;
}
}
FreePool (FileInfo);
CurrentFile->Close (CurrentFile);
VolumeFile->Close (VolumeFile);
return EFI_SUCCESS;
}
我的建议:在实际项目中,不要自己写遍历逻辑。UEFI提供了EFI_FILE_PROTOCOL的Open()和Read(),直接用就行。但要注意,文件名是GUID字符串,不是人类可读的名字。
3.7 避坑指南
最后,分享几个我这些年踩过的坑:
- 校验和:修改固件文件后,一定要重新计算FFS头部的校验和。否则固件会拒绝加载。
- 对齐:FFS要求文件起始地址对齐到8字节(或16字节,取决于FV属性)。不对齐会导致解析失败。
- 空闲空间填充:FV里未使用的空间必须填充为0xFF。这是Flash的特性决定的(擦除后为0xFF)。
- Section嵌套:压缩Section里可以嵌套其他Section。解析时要递归处理。
- GUID冲突:确保你自定义的GUID不与其他标准GUID冲突。我见过有人用了
DXE_CORE的GUID,结果系统启动时加载了两个Core,直接崩溃。
嗯,这一章的内容就到这里。FFS、FV、固件文件这些概念,是理解UEFI固件的基础。你只要记住:BIOS ROM是一个或多个FV的集合,每个FV里包含多个固件文件,每个文件又由多个Section组成。而FFS协议,就是你在运行时访问这些文件的钥匙。