3、UEFI固件结构:Flash布局、FV与FFS

好,咱们今天聊聊UEFI固件在Flash里到底是怎么躺着的。很多人写驱动、调BIOS,天天跟FV、FFS打交道,但真要问一句“Flash里从0地址开始到底放了些什么”,能说清楚的人其实不多。我当年刚入行时也踩过这个坑——改了个DXE驱动,刷进去机器直接变砖,后来才发现是Firmware Volume的布局被我搞乱了。

说白了,UEFI固件不是简单地把一个二进制文件扔进Flash就完事。它有一套自己的“文件系统”,叫Firmware File System(FFS),专门用来管理固件里的各种模块。而Firmware Volume(FV)就是这套文件系统的“容器”。

3.1 Flash的整体布局

先看宏观。一个典型的UEFI BIOS Flash,从低地址到高地址,大致分这么几块:

区域 起始偏移(示例) 内容
SEC Core / Reset Vector 0xFFFFFFF0(Top Swap) CPU上电第一条指令所在,跳转到SEC阶段入口
PEI FV 靠近Flash顶部 PEI阶段需要的模块:PEI Core、PEIMs、S3 Resume等
DXE FV 中间区域 DXE阶段驱动、Protocol、UEFI Application等
NVRAM / Variable区域 底部或独立分区 UEFI变量、Boot Option、Setup数据
Fault Tolerant区域 紧邻NVRAM 用于保证变量更新不因掉电而损坏
Descriptor / IFWI Flash最底部 Intel Flash Descriptor,包含PCH配置、ME固件等
个人习惯:我每次拿到一块新板子,第一件事就是读Flash Descriptor,看看Flash分区表是怎么划分的。这能帮你避免很多“刷错区域”的低级错误。

你可能会问:“为什么PEI FV要放在靠近顶部的地方?”嗯,这跟CPU的Reset Vector有关。x86 CPU上电后,第一条指令从4GB - 16字节(0xFFFFFFF0)开始取指。所以Flash的顶部必须映射到那个地址。SEC阶段的代码就放在那里,它负责最基础的CPU初始化,然后找到PEI Core并跳转过去。

3.2 Firmware Volume(FV)——固件的“分区”

FV可以理解为一个逻辑容器。一个Flash里可以有多个FV,每个FV有自己独立的头部和文件系统。常见的FV有:

  • SECFV:存放SEC阶段代码,通常很小,几KB到几十KB。
  • PEIFV:存放PEI Core和PEIMs。
  • DXEFV:存放DXE驱动和UEFI应用,这是最大的一个FV。
  • FVMAIN:有些平台把PEI和DXE合并到一个FV里,叫FVMAIN。
  • NVRAM FV:专门用来存UEFI变量的FV,格式特殊。

每个FV都有一个FV Header,里面记录了FV的GUID、大小、文件系统版本、属性(比如是否支持擦除、是否对齐等)。我建议你拿到一个BIOS ROM后,用工具(比如UEFITool)打开,先看FV Header里的FvLength和FvBlockMap,这能告诉你这个FV有多大、每个擦除块多大。

关键点:FV的擦除块大小(Block Size)必须跟Flash芯片的物理擦除块对齐。如果不对齐,固件更新时可能会擦掉不该擦的数据。我曾经见过一个案例,就是因为FV的Block Map写错了,导致刷固件时把NVRAM区域也给擦了,用户的所有设置全部丢失。

3.3 Firmware File System(FFS)——FV里的“文件系统”

FV内部的文件系统就是FFS。它跟FAT32、NTFS这些文件系统有点像,但更简单、更专一。FFS里存放的基本单元叫Firmware File(固件文件)。每个文件由三部分组成:

  1. File Header:固定大小(通常是32字节或48字节),包含文件名(GUID)、文件类型、属性、大小、校验和等。
  2. File Body:实际的数据内容。可以是PE32+镜像、TE镜像、原始二进制、压缩数据等。
  3. Pad / Alignment:文件末尾的填充字节,用于对齐到下一个16字节或8字节边界。

FFS支持的文件类型主要有:

文件类型 值 说明
EFI_FV_FILETYPE_RAW 0x01 原始二进制数据,比如微码、Logo图片
EFI_FV_FILETYPE_FREEFORM 0x02 自由格式,通常用于存放ACPI表、SMBIOS等
EFI_FV_FILETYPE_SECURITY_CORE 0x03 SEC Core文件
EFI_FV_FILETYPE_PEI_CORE 0x04 PEI Core文件
EFI_FV_FILETYPE_DXE_CORE 0x05 DXE Core文件
EFI_FV_FILETYPE_PEIM 0x06 PEI模块(PEIM)
EFI_FV_FILETYPE_DRIVER 0x07 DXE驱动
EFI_FV_FILETYPE_APPLICATION 0x09 UEFI应用程序(如Shell)
EFI_FV_FILETYPE_FIRMWARE_VOLUME_IMAGE 0x0B 嵌套的FV镜像,即FV里套FV
避坑指南:我曾经在调试一个UEFI网络驱动时,发现驱动加载后总是报“Image Corrupted”。查了两天才发现,是FFS文件头里的校验和(Checksum)算错了。UEFI规范要求File Header的16位校验和加上所有字节的和必须为0。很多工具链会自动计算,但如果你手动修改过二进制,一定要重新算一遍校验和。

3.4 FFS的布局与文件对齐

FFS在FV里是连续存放的。文件之间没有目录表,也没有索引。那怎么找到文件呢?答案是:从头扫描。从FV的起始地址开始,读第一个文件的Header,根据Header里的Size字段跳到下一个文件,直到遇到FFS的结束标记(通常是全0xFF的填充区域)。

这种设计的好处是简单、可靠。坏处是查找文件慢——但固件里的文件数量通常不多(几十到几百个),所以影响不大。

文件对齐是个容易忽略的细节。UEFI规范要求:

  • 文件起始地址必须对齐到8字节(64位)边界。
  • 如果FV属性里设置了EFI_FVB2_ALIGNMENT_16,则要对齐到16字节。
  • 有些FV要求文件对齐到1KB甚至4KB。

对齐不对,轻则固件更新失败,重则启动时直接挂掉。我记得有一次,一个同事把DXE驱动编译出来直接塞进FV,没注意对齐,结果PEI阶段解压FV时直接报错,机器连Logo都看不到。

3.5 嵌套FV与压缩FV

实际项目中,FV可以嵌套。比如一个FV里放一个类型为EFI_FV_FILETYPE_FIRMWARE_VOLUME_IMAGE的文件,这个文件的Body本身又是一个完整的FV。这种嵌套常用于:

  • 把PEI和DXE分开管理。
  • 存放恢复用的备份固件。
  • 存放平台特定的配置数据。

另外,很多BIOS会对FV进行压缩。压缩后的FV文件类型仍然是EFI_FV_FILETYPE_FIRMWARE_VOLUME_IMAGE,但文件属性里会标记为“压缩”。PEI阶段有专门的解压驱动(如LzmaDecompress)来解压这些FV。压缩比通常能到50%左右,对于Flash空间紧张的嵌入式设备来说,这招很实用。

我建议:如果你在调试启动流程,遇到“FV not found”或“Decompress failed”之类的错误,先别急着怀疑代码。用UEFITool打开固件,看看FV的嵌套结构和压缩标记对不对。很多时候,问题出在固件打包阶段,而不是代码逻辑。

3.6 一张图看懂整体结构

下面这张SVG图,把Flash布局、FV、FFS三者的关系画清楚了。你可以把它当作本章的“地图”。

UEFI固件Flash布局与FV/FFS结构 UEFI固件Flash布局与FV/FFS结构 Flash芯片 (物理存储) Descriptor / IFWI Flash描述符、ME固件 NVRAM区域 UEFI变量 Boot Option DXE FV DXE Core DXE Drivers UEFI Apps PEI FV PEI Core PEIMs SEC Core Reset Vector Firmware Volume (FV) 逻辑容器 + 文件系统 FV Header GUID, Size, BlockMap, Attributes FFS(Firmware File System) File 1: PEI Core Header + Body (TE镜像) File 2: DXE Driver Header + Body (PE32+镜像) File 3: Logo Raw Header + Body (BMP数据) File 4: Nested FV Header + Body (压缩FV) ... 更多文件 + 填充区域 单个FFS文件结构 File Header(32/48字节) GUID | Type | Attributes | Size State | Checksum | Alignment File Body(实际数据) PE32+ / TE / Raw / 压缩数据 Section 1: Dependency Section 2: PE32 Image Section 3: UI String Section 4: Version Padding(对齐填充) 0xFF 填充到对齐边界 关键要点 • FV是容器,FFS是文件系统 • 文件通过GUID唯一标识 • 文件按顺序扫描,无目录表 • 对齐错误会导致启动失败 • 支持嵌套FV和压缩FV

从这张图可以看得很清楚:Flash芯片里划分了多个区域,每个FV是一个独立的逻辑容器,FV内部通过FFS来组织文件。每个文件又有自己的Header和Body,Body里可以包含多个Section(比如依赖表、镜像数据、UI字符串等)。

3.7 实际项目中的注意事项

最后,分享几个我在项目中积累的经验:

  • FV大小必须对齐到Block Size的整数倍。如果FV大小不是Block Size的整数倍,固件更新工具可能会拒绝写入,或者在写入时破坏相邻区域的数据。
  • NVRAM FV的布局有特殊要求。它必须放在Flash的末尾附近,而且要有Fault Tolerant机制。我见过不少因为NVRAM布局不对,导致变量写入时掉电,然后机器再也起不来的案例。
  • 调试时多用UEFITool。这个工具可以直观地看到FV和FFS的树形结构,还能提取、替换、删除文件。我几乎每天都会用它。
  • 注意文件状态(State)。FFS文件Header里有一个State字段,表示文件是否有效、是否被删除、是否被标记等。固件更新时,如果State没更新对,可能会导致文件被误认为无效。
曾经有一次:我在调试一个S3 Resume的问题,发现每次从睡眠唤醒后,系统都会卡在PEI阶段。查了很久,最后发现是PEI FV里有一个PEIM的文件State被错误地标记为“Deleted”。PEI Core扫描文件时跳过了它,导致某个关键硬件没初始化。修复方法很简单——重新生成固件,确保所有文件的State都是“Valid”。但排查过程花了我整整两天。

嗯,关于Flash布局、FV和FFS,今天就聊这么多。这些东西看起来是基础,但如果你能真正理解它们,调试固件时会少走很多弯路。下次遇到“固件刷进去起不来”的问题,不妨先打开UEFITool,看看FV和FFS的结构对不对。