第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的完整层级关系。

UEFI固件架构层级图 UEFI固件架构层级关系 BIOS ROM (SPI Flash) FV 1 (主区域) FV 2 (恢复区域) FV 3 (NVRAM) 固件文件 (Firmware File) GUID: 12345678-... (DXE Core) 类型: EFI_FV_FILETYPE_DRIVER 固件文件 (Firmware File) GUID: 87654321-... (Logo.bmp) 类型: EFI_FV_FILETYPE_RAW Section 1 PE32 Section 2 DEPEX Section 1 RAW Section 2 GUID Defined 说明 FFS协议提供OpenVolume接口,返回Volume Handle;通过EFI_FILE_PROTOCOL遍历文件。

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协议,就是你在运行时访问这些文件的钥匙。