UEFI固件基础:固件类型、存储布局与文件系统

好,我们正式开始第二讲。上一章我们聊了UEFI的宏观定位,这一章得扎到固件内部去看看。说白了,就是搞清楚UEFI固件到底长什么样,由哪些阶段组成,代码和数据又是怎么塞进Flash里的。

我个人习惯把UEFI固件理解成一个「微型操作系统」。它有自己的启动流程、内存管理、甚至文件系统。嗯,这个文件系统就是FFS(Firmware File System)。你想想看,BIOS的Flash空间就那么几兆到几十兆,怎么高效地组织成千上万个模块?这就是我们今天要讲的核心。

2.1 固件类型:PI、PEI、DXE、BDS

UEFI规范定义了一套标准的启动阶段。我当年刚接触时,总觉得这些阶段名字很绕。后来在项目里调过几次启动失败的问题,才真正理解它们各自的分工。

先看这张图,把整体流程串起来:

UEFI 固件启动阶段与核心职责 SEC 安全验证 · 初始化Cache PEI 内存初始化 · 资源发现 DXE 驱动调度 · 协议注册 BDS 启动策略 · 设备枚举 PI(Platform Initialization)规范定义了 SEC → PEI → DXE → BDS 的标准框架 各阶段通过 HOB、Protocol、Handle Database 传递数据和状态 复位向量 临时内存 完整内存 启动设备 ← 硬件依赖逐渐降低,抽象层次逐渐升高 → PI 规范定义(框架层) SEC + PEI + DXE + BDS UEFI 规范定义(接口层) Boot Services + Runtime Services + Protocols

2.2 各阶段深度解析

SEC(安全验证阶段)

这是CPU复位后执行的第一段代码。空间极小,通常只有几个KB。它的任务很简单:验证下一个阶段(PEI)的镜像是否完整,然后跳转。我在一个国产平台项目里遇到过,SEC阶段代码里有个栈指针初始化错了,导致PEI阶段一调用函数就崩。查了两天才定位到,嗯,从那以后我写SEC代码都会反复检查栈布局。

PEI(EFI前初始化阶段)

PEI的核心目标是:把内存初始化出来。你想想看,没有内存,C语言跑不了,复杂逻辑写不了。PEI阶段运行在所谓的「临时内存」上——通常是CPU的Cache配置成RAM(CAR,Cache As RAM)。

  • PEI Foundation:核心调度器,负责分发PPI(PEIM-to-PEIM Interface)
  • PEIM(PEI Module):一个个功能模块,比如内存控制器初始化、CPU微码更新
  • HOB(Hand-Off Block):PEI阶段产出的数据结构,传递给DXE。我建议你把它理解成「接力棒」

关键认知:PEI阶段结束时,系统必须拥有完整的系统内存。HOB列表里必须包含内存描述符,否则DXE无法启动。

DXE(驱动执行环境)

DXE是UEFI固件里最庞大的阶段。它运行在完整的内存环境中,可以加载复杂的驱动。DXE的核心机制是「协议(Protocol)」——每个驱动注册自己支持的协议,其他驱动通过协议来调用服务。

我记得有一次调试一个NVMe驱动加载失败的问题。DXE阶段枚举了所有设备,但NVMe控制器始终没有绑定上驱动。最后发现是某个Protocol的GUID在驱动里写错了。一个字节的差异,查了我整整一个下午。

阶段 内存状态 核心机制 产出物
SEC 无(直接操作寄存器) 安全验证、跳转 SEC映像哈希值
PEI Cache as RAM(CAR) PPI、HOB HOB列表
DXE 完整系统内存 Protocol、Handle Database Boot Manager就绪
BDS 完整系统内存 Boot Policy、Driver Order 启动项选择

BDS(启动设备选择阶段)

BDS阶段说白了就是「决定从谁启动」。它读取固件设置中的启动顺序,依次尝试每个启动项。如果某个启动项失败(比如硬盘没插好),就跳到下一个。

这里有个常见的坑:BDS阶段可能会因为某个USB设备枚举超时,导致整个启动流程卡住几十秒。我曾经在一个工控项目里遇到过,客户抱怨开机太慢。最后发现是BDS阶段枚举了一个坏掉的USB Hub,每次都要等到超时才会跳过。

避坑指南:BDS阶段的超时处理一定要做容错。我曾经见过一个固件,因为某个启动项的设备返回了错误状态,BDS直接死循环了。加一个「最大重试次数」就能解决。

2.3 固件存储布局:Firmware Volume(FV)

好,阶段讲完了。那这些代码和数据存在哪里?答案是Firmware Volume(固件卷,简称FV)。

FV是Flash上的一个逻辑容器。你可以把它想象成一个「分区」。一个Flash里可以有多个FV,每个FV有自己的GUID、大小、对齐方式。FV内部存放的是FFS文件。

FV的头部结构长这样:

typedef struct {
  UINT8   ZeroVector[16];    // 必须全零
  EFI_GUID FileSystemGuid;   // 文件系统类型标识
  UINT64  FvLength;          // FV总长度
  UINT32  Signature;         // '_FVH' 签名
  UINT32  Attributes;        // 属性位(对齐、擦除策略等)
  UINT16  HeaderLength;      // 头部长度
  UINT16  Checksum;          // 校验和
  UINT8   Reserved[1];
  UINT8   Revision;          // 版本号
  // ... 更多字段
} EFI_FIRMWARE_VOLUME_HEADER;

我个人习惯把FV的布局画成下面这样:

Firmware Volume 典型布局 Firmware Volume(FV) FV Header(56字节) FFS Files 区域 File 1 File 2 File N Free Space(空闲空间) FV内部文件按GUID索引,支持对齐、校验和完整性检查

2.4 固件文件系统:FFS

FFS(Firmware File System)是FV内部的组织方式。每个FFS文件包含一个头部和若干数据段(Section)。

FFS文件头结构:

typedef struct {
  EFI_GUID  NameGuid;        // 文件唯一标识
  UINT8     IntegrityCheck;  // 完整性校验
  UINT8     Type;            // 文件类型(RAW、PE32、TE等)
  UINT8     Attributes;      // 属性(对齐、固定等)
  UINT8     Size[3];         // 文件大小(24位)
  UINT8     State;           // 状态(擦除、写入、有效等)
} EFI_FFS_FILE_HEADER;

常见的文件类型:

  • EFI_FV_FILETYPE_RAW:原始二进制数据,比如微码补丁
  • EFI_FV_FILETYPE_PE32:PE32+格式的可执行镜像,DXE驱动常用
  • EFI_FV_FILETYPE_TE:Terse Executable,一种精简的PE格式,PEI阶段常用
  • EFI_FV_FILETYPE_FREEFORM:自由格式,用于存放ACPI表等

实用技巧:调试时可以用UEFI固件工具(比如UEFITool)直接打开BIOS镜像,查看FV内部的FFS文件结构。我曾经用这个工具定位过一个ACPI表被错误覆盖的问题——直接在工具里对比两个版本的固件,发现某个FFS文件的GUID重复了。

2.5 总结与核心要点

这一章的内容比较多,我帮你梳理几个必须记住的点:

  1. PI规范定义了框架:SEC → PEI → DXE → BDS,每个阶段有明确的分工和产出
  2. FV是存储容器:Flash上的逻辑分区,包含头部和FFS文件
  3. FFS是文件系统:每个文件有GUID、类型、状态,内部可以包含多个Section
  4. HOB是阶段间的桥梁:PEI产出的HOB列表,DXE和BDS都依赖它

嗯,这些概念听起来可能有点抽象。但等你真正开始写固件代码、调试启动流程时,你会发现这些设计其实非常合理。每个阶段解决一个特定问题,每个数据结构都有明确的用途。