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固件等 |
你可能会问:“为什么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有多大、每个擦除块多大。
3.3 Firmware File System(FFS)——FV里的“文件系统”
FV内部的文件系统就是FFS。它跟FAT32、NTFS这些文件系统有点像,但更简单、更专一。FFS里存放的基本单元叫Firmware File(固件文件)。每个文件由三部分组成:
- File Header:固定大小(通常是32字节或48字节),包含文件名(GUID)、文件类型、属性、大小、校验和等。
- File Body:实际的数据内容。可以是PE32+镜像、TE镜像、原始二进制、压缩数据等。
- 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 |
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空间紧张的嵌入式设备来说,这招很实用。
3.6 一张图看懂整体结构
下面这张SVG图,把Flash布局、FV、FFS三者的关系画清楚了。你可以把它当作本章的“地图”。
从这张图可以看得很清楚: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没更新对,可能会导致文件被误认为无效。
嗯,关于Flash布局、FV和FFS,今天就聊这么多。这些东西看起来是基础,但如果你能真正理解它们,调试固件时会少走很多弯路。下次遇到“固件刷进去起不来”的问题,不妨先打开UEFITool,看看FV和FFS的结构对不对。