3. UEFI固件基础:固件类型与存储布局
这一章我们聊聊固件的骨架——PI、PEI、DXE、BDS这几个阶段,以及固件到底是怎么在Flash里躺着的。说白了,就是搞明白UEFI固件从加电到交出控制权,中间经历了什么,代码和数据又存在哪里。
我刚开始接触UEFI时,也被这些缩写搞得头大。后来亲手调过几次板子,才真正理解它们的分工。嗯,咱们一步步来。
3.1 固件类型:PI、PEI、DXE、BDS
UEFI固件不是一个大泥球。它被分成了几个清晰的阶段,每个阶段干不同的事。这个设计思路,我个人觉得非常优雅——把复杂问题拆解了。
3.1.1 PI(Platform Initialization)—— 平台初始化的总纲
PI是Intel定义的平台初始化规范。它不直接写代码,而是规定了固件该怎么分层、各阶段怎么交接。你可以把它理解成一张蓝图。
PI规范把固件分成了几个阶段:
- SEC(Security):安全验证,加电后第一个执行的代码
- PEI(Pre-EFI Initialization):早期初始化,内存还没好时干的事
- DXE(Driver Execution Environment):驱动执行环境,内存有了,大干一场
- BDS(Boot Device Selection):启动设备选择,决定从哪启动
我在项目中见过不少团队把PI和UEFI搞混。其实PI是平台初始化,UEFI是操作系统和固件的接口。两者配合,但分工不同。
3.1.2 PEI(Pre-EFI Initialization)—— 内存出来前的“荒野求生”
PEI阶段最大的特点:没有内存可用。你想想看,连C语言的栈都跑不了,代码只能靠汇编和极有限的临时存储来执行。
PEI的核心任务:
- 初始化CPU、芯片组的基本配置
- 找到并初始化内存控制器
- 把内存信息传递给下一阶段
PEI里跑的是PEIM(PEI Module)。每个PEIM负责一小块功能。比如有个PEIM专门初始化内存,另一个专门检测CPU频率。
3.1.3 DXE(Driver Execution Environment)—— 内存充裕后的“大展拳脚”
内存初始化成功后,固件进入DXE阶段。这时候C语言可以正常跑了,栈、堆都有了。DXE阶段会加载大量的DXE驱动,完成大部分硬件初始化。
DXE阶段干的事:
- 枚举PCI总线,初始化所有PCI设备
- 加载各种总线驱动、设备驱动
- 创建UEFI运行时服务、启动服务
- 准备启动环境
DXE驱动之间通过Protocol通信。Protocol是UEFI里的接口机制,类似面向对象里的接口。一个驱动生产Protocol,另一个驱动消费它。
// 一个典型的DXE驱动入口
EFI_STATUS
EFIAPI
MyDriverEntryPoint(
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
// 注册Protocol,或者消费其他Protocol
// 初始化硬件
return EFI_SUCCESS;
}
我个人习惯把DXE阶段想象成一个“插件系统”。每个驱动是一个插件,系统启动时按需加载。这种设计让固件非常灵活。
3.1.4 BDS(Boot Device Selection)—— 决定从哪启动
BDS是固件启动流程的最后一站。它的任务很简单:找个设备启动操作系统。
BDS会做这些事:
- 读取启动顺序(BootOrder)
- 遍历启动选项,尝试启动
- 如果所有选项都失败,进入固件设置界面
BDS阶段会用到Boot Manager。Boot Manager根据NVRAM里保存的配置,决定先试U盘、再试硬盘、还是网络启动。
3.2 固件存储布局:Firmware Volume
固件代码存在哪?存在Flash芯片里。但Flash不是随便乱放的,它被组织成Firmware Volume(FV)。FV是固件存储的基本单位。
3.2.1 什么是Firmware Volume
Firmware Volume(FV)是一个逻辑容器,里面存放了固件的各种文件。你可以把它想象成一个“只读文件系统”,专门为固件设计的。
一个FV包含:
- FV Header:描述FV的基本信息,比如大小、校验和、版本
- Firmware File System(FFS):文件系统,管理FV里的文件
- Firmware Files:实际存放的固件文件,比如PEIM、DXE驱动、数据等
3.2.2 FV的布局结构
一个典型的固件Flash里,会有多个FV。每个FV负责不同的阶段:
| FV名称 | 存放内容 | 典型大小 |
|---|---|---|
| FV_MAIN | DXE驱动、BDS代码、UEFI应用 | 2MB - 8MB |
| FV_PEI | PEIM、早期初始化代码 | 128KB - 512KB |
| FV_SMM | SMM(系统管理模式)代码 | 64KB - 256KB |
| FV_NVRAM | NVRAM变量存储 | 32KB - 128KB |
这些FV在Flash里是连续排列的。Flash的最开头通常是SEC阶段代码,它不放在FV里,而是直接放在Flash的起始位置。因为加电后CPU直接从这里取指令。
3.2.3 FFS(Firmware File System)
FV内部的文件系统叫FFS。FFS管理着FV里的所有文件。每个文件都有一个唯一的GUID作为标识。
FFS文件类型:
- PEIM:PEI阶段的可执行文件
- DXE驱动:DXE阶段的可执行文件
- 应用程序:UEFI应用程序,比如Shell
- 原始数据:比如Logo图片、ACPI表、配置数据
- FFS Padding:填充数据,用于对齐
每个FFS文件由两部分组成:
- File Header:包含GUID、类型、大小、校验和
- File Data:实际内容
FFS的设计很巧妙。它支持文件级校验,可以检测固件是否被损坏。我在调试时经常用这个特性来确认固件刷写是否正确。
3.2.4 固件存储的实际布局
一个典型的UEFI固件在Flash里的布局大概是这样:
Flash起始地址
+-----------------------------+
| SEC阶段代码(非FV格式) | ← 加电后第一条指令
+-----------------------------+
| FV_PEI |
| - PEIM 1 |
| - PEIM 2 |
| - ... |
+-----------------------------+
| FV_MAIN |
| - DXE驱动 1 |
| - DXE驱动 2 |
| - BDS代码 |
| - UEFI Shell |
| - ... |
+-----------------------------+
| FV_SMM |
| - SMM驱动 |
+-----------------------------+
| FV_NVRAM |
| - NVRAM变量 |
+-----------------------------+
| 其他数据(Logo、ACPI等) |
+-----------------------------+
Flash结束地址
这个布局不是固定的。不同厂商、不同平台会有差异。但基本思路一致:按阶段分FV,按功能分文件。
3.3 各阶段的关系与数据传递
PEI、DXE、BDS不是孤立的。它们之间有数据传递机制。
- PEI到DXE:通过HOB(Hand-Off Block)传递。HOB是一块内存区域,PEI阶段把内存信息、CPU信息等写进去,DXE阶段读取。
- DXE到BDS:通过Protocol和事件。DXE驱动注册Protocol,BDS通过Protocol找到启动设备。
- BDS到OS:通过UEFI启动服务。BDS调用启动服务,加载OS引导程序。
HOB是个好东西。它是一段连续的内存,里面存放了各种数据块。每个数据块有类型和长度。PEI阶段往HOB里写数据,DXE阶段读出来用。
// HOB的结构
typedef struct {
UINT16 HobType; // HOB类型
UINT16 HobLength; // HOB长度
UINT32 Reserved;
} EFI_HOB_GENERIC_HEADER;
// 内存信息HOB示例
typedef struct {
EFI_HOB_GENERIC_HEADER Header;
EFI_PHYSICAL_ADDRESS PhysicalStart;
UINT64 ResourceLength;
// ...
} EFI_HOB_RESOURCE_DESCRIPTOR;
我记得第一次调试HOB时,发现PEI阶段写的数据在DXE阶段读不到。查了半天,原来是HOB的校验和计算错了。从那以后,我每次写HOB都会仔细检查校验逻辑。
3.4 本章小结
这一章我们聊了固件的类型和存储布局。PEI、DXE、BDS各司其职,Firmware Volume是固件的“文件系统”。理解这些基础,你才能看懂固件是怎么跑起来的。
下一章我们会深入SEC阶段,看看加电后第一条指令到底干了什么。嗯,那是整个启动流程的起点。