一、PEI Core 与 PEIMs
PEI Core:是PEI阶段的核心引擎,负责寻找,加载和执行各种PEIM。PEI Core首先首先运行一个小的固定的SEC阶段的代码,这段代码通常使用CPU缓存作为临时内存来运行。一旦找到并初始化了真正的系统内存,PEI核心就会把执行权交给永久内存管理器,从而结束使用CAR。
PEIMs:这些是实际执行硬件初始化任务的功能模块。执行芯片组、内存控制器、桥接器和核心主板组件的最低限度初始化。关键任务是初始化 DRAM,使其可用于后续的启动阶段。创建 Hand-Off Blocks (HOB) 列表,这是一个数据结构,用于保存所有已发现的硬件状态、配置信息和资源映射,并将这些信息传递给下一个启动阶段------DXE 阶段。
下面对这两部分的流程进行更具体的介绍:
PEI Core 主要是提供一个基本的执行环境然后找到并执行 PEIMs,它才是真正干活的。
| 关键职责 | 具体任务 | 对应的概念/API |
|---|---|---|
| A. 环境建立 | 在 SEC 阶段交接后,立即建立 PEI Core 自身的堆栈和数据结构(最初在 CAR 中)。 | CAR (Cache As RAM) |
| B. 服务提供 | 提供 PEI 阶段的基本服务(如内存分配、HOB 创建)。 | PEI Services Table |
| C. PEIM 调度 | 搜索固件存储空间 (FV) 以发现待执行的 PEIM 映像,并按顺序执行它们。 | FV (Firmware Volume) |
| D. 内存转换 | 一旦 DRAM 被初始化,它将所有 PEI 结构从 CAR 迁移到永久内存中。 | PeiServicesInstallPeiMemory() |
| E. 阶段交接 | 所有 PEIMs 执行完毕后,构建最终的 HOB 列表,并跳转到 DXE Core。 | DXE IPL (Initial Program Load) |
PEIMs 是实际的工作执行者,它们通过**"提供 PPI"** (PEIM-to-PEIM Interface)和**"注册 Notify"**机制来完成工作和进行协作。
| 关键职责 | 具体任务 | 对应的机制 |
|---|---|---|
| A. 硬件初始化 | 初始化芯片组、时钟、电源管理等关键硬件。 | PCHInitPeim , MemoryInitPeim |
| B. 资源发现 | 找到并配置系统内存 (DRAM),并报告给 PEI Core。 | Memory Init PEIM |
| C. 数据发布 | 创建并安装 PPI (PEI Protocol Interface),让其他 PEIM 知道某个服务或数据已可用。 | PeiServicesInstallPpi() |
| D. 阶段准备 | 收集硬件数据,创建 HOB,将配置信息传递给 DXE。 | PeiServicesCreateHob() |
伪代码:
c
// -------------------------------------------------------------------------
// 阶段 0: 接管和临时环境建立 (运行在 CAR/Cache As RAM)
// -------------------------------------------------------------------------
Function PeiCoreMain(SecCoreData, HandoffData)
// 1. 设置 PEI Services Table
PeiServicesTable = InitializePeiServicesTable()
// 2. 初始化临时堆栈和临时堆 (基于CAR)
InitializePeiCoreStack(SecCoreData.StackBase)
InitializePeiTemporaryMemoryHeap()
// 3. 创建第一个 HOB: Handoff Information HOB
CreateHob(HOB_TYPE_HANDOFF, HandoffData)
// 4. 初始化 PEIM 调度器
PeimDispatcher = InitializePeimDispatcher()
// 5. 开始 PEIM 主循环
Call PeimExecutionLoop(PeimDispatcher, PeiServicesTable)
// 6. 退出 PEI 阶段
Call PeiCoreExit()
End Function
// -------------------------------------------------------------------------
// 阶段 1: PEIM 调度与执行主循环
// -------------------------------------------------------------------------
Function PeimExecutionLoop(Dispatcher, Services)
// 循环条件: 只要在 Firmware Volume (FV) 中还能找到未执行的 PEIM
While (Dispatcher.FindNextPeimImage() != NULL)
CurrentPeim = Dispatcher.FindNextPeimImage()
// 1. 加载 PEIM 代码
CurrentPeim.Image = Services.FfsFindPeim(CurrentPeim.FilePath)
// 2. 执行 PEIM 的入口点 (EntryPoint)
// 实际的硬件初始化和 PPI 安装都在这里发生
Status = CurrentPeim.EntryPoint(CurrentPeim.Image, Services)
If (Status == EFI_SUCCESS)
MarkPeimAsExecuted(CurrentPeim)
// 3. 检查是否有内存初始化发生
If (Services.IsMemoryInstalled() == TRUE)
// 内存初始化 PEIM 已经运行,进入关键的转换流程
Call PeimMemoryTransition()
// 由于内存转换,代码和数据可能已被迁移,需要重新启动循环
// 通常会重新设置 PEI Core 的环境指针
Return // 退出当前循环,进入下一个 PEI 阶段
End If
Else
Log(ERROR, "PEIM failed to execute: " + CurrentPeim.Name)
End If
End While
End Function
// -------------------------------------------------------------------------
// 阶段 2: 内存转换 (最关键的一步)
// -------------------------------------------------------------------------
Function PeimMemoryTransition()
// 1. 发现新安装的永久内存信息
ResourceHob = Services.GetHobByType(HOB_TYPE_RESOURCE_DESCRIPTOR)
// 2. 告诉 PEI Services Table 内存已安装
Services.PeiServicesInstallPeiMemory()
// 3. 将 PEI Core 自身的数据、PEIMs 和 HOB 列表
// 从临时的 CAR 空间迁移到新的永久内存中
MigratePeiCoreDataToDRAM()
// 4. 更新 Services Table 和所有 PEIMs 的指针
UpdatePointersToNewDRAMLocations()
// 5. 重新进入 PeimExecutionLoop,寻找需要二次执行的 PEIMs (Post-Memory PEIMs)
Call PeimExecutionLoop(...)
End Function
// -------------------------------------------------------------------------
// 阶段 3: 退出 PEI 阶段
// -------------------------------------------------------------------------
Function PeiCoreExit()
// 1. 确保所有 HOB 都已创建,生成最终的 HOB 列表
FinalizeHobList()
// 2. 查找并加载 DXE IPL PEIM (Initial Program Load)
DxeIplPeim = FindPeim("DxeIpl.inf")
// 3. 执行 DXE IPL PEIM,其工作是加载 DXE Core 映像
// 并最终将控制权交给 DXE Core
DxeIplPeim.EntryPoint(HobListAddress)
// 永远不会返回 (Never Returns), 因为控制权已转交给 DXE Core
End Function
-
PEI Core 负责整个流程的框架和调度(
PeiCoreMain)。 -
PEIM 在
PeimExecutionLoop中被加载和执行,完成实际的硬件配置,是真正干活的。 -
内存转换是分水岭。
PeimMemoryTransition标志着系统从使用 CPU 缓存运行(CAR)过渡到使用真正的 DRAM 内存。这是整个 PEI 阶段最关键的一步。 -
HOB 是最终阶段结果和目的。最终的 HOB 列表是 PEI 阶段工作的产物,作为参数传递给 DXE 阶段。
二、PEIMs 在 EDKII 中的体现
第一节中我们提到,PEI 会搜索固件卷(FV)中待执行的的 PEIM 镜像(Imaga),并按照顺序执行。这里可以先简单把 FV 理解为文件夹,文件夹中存放着特定的功能性程序,也就是 PEIM 镜像。下面举个例子介绍 PEIMs 在EDKII 工程中的形式。
在 EDKII 中,PEIM 本质上就是一个模块(Module),其通常被编译为.efi适用于 PEI 的 PE/COFF 可执行文件,且最终被放入 FV 中,一般是 FV/PEIFV 或 FV/MAINFV。
有关 EDKII 工程结构的介绍异步此博客。一般情况下,PEIM 的源代码位于某个 Package 的目录中,典型路径(以 MdeModulePkg 中的示例为例):
sh
edk2/
└── MdeModulePkg/
└── Universal/
└── PCD/
└── Pei/
├── Pcd.inf ← PEIM 的 INF 文件(最重要)
├── Pcd.c ← 源码
└── Pcd.h
➡️ 所有 PEIM 模块的特征是:INF 文件中有:
inf
[Defines]
MODULE_TYPE = PEIM
有关模块的概念后续会补充,前面章节中有关于MdeModulePkg的介绍以及单独编译某模块的方法,还有模块的配置描述文件inf文件的简单介绍,感兴趣可以移步!
PEIM 是如何被编译的呢?首先在 EDKII 工程中,我们最终的目的是编译某个平台的固件文件,比如虚拟机使用的 ovmf.fd,其平台描述文件位于OvmfPkg/OvmfPkgX64.dsc。具体内容点击此处在附录中有介绍。这个平台描述文件会定义哪些 PEIM 模块会被编译。fdf文件OvmfPkg/OvmfPkgX64.fdf描述了最终的固件镜像是如何布局的,因此PEIM 是否进入固件由平台 FDF 文件决定。
fdf文件的介绍后续会另外补充!
例如 OvmfPkgX64.dsc 内会有:
sh
[MdeModulePkg/Universal/PCD/Pei/Pcd.inf]
只要在 .dsc 里被包含,构建系统就会编译它。
编译 ovmf 固件镜像时
sh
build -a X64 -t GCC5 -p OvmfPkg/OvmfPkgX64.dsc
编译操作自动会:找到所有 INF(包括 PEIM),生成 .obj,链接出 PEIM 的 .efi(PEI 可执行)。
EDK II 的编译输出统一放在 Build/ 目录中。比如
sh
Build/OvmfX64/DEBUG_GCC5/X64/MdeModulePkg/Universal/PCD/Pei/Pcd/OUTPUT/Pcd.efi
PEIM 在最终固件镜像(FV)里:
sh
Build/OvmfX64/DEBUG_GCC5/FV/
├── OVMF.fd
├── OVMF_CODE.fd
├── OVMF_VARS.fd
└── PEIFV.Fv # efi 文件会被工具打包进 .FV 文件。
可以查看 FDF 文件,例如 OvmfPkgX64.fdf:
sh
FV = PEIFV {
INF MdeModulePkg/Universal/PCD/Pei/Pcd.inf
}
这表示 Pcd.inf (一个 PEIM)会放入该 Firmware Volume。
三、PEI Services Table 介绍
在 PEI Core 以及伪代码中涉及到 PEI Services Table 这里简单介绍一下。
PEI Services Table 本质上是一个指向一系列函数指针的结构体。它允许 PEIMs 访问底层 PEI Core 实现的服务,而无需知道这些服务的具体位置和内部细节。通俗来说就相当于执行具体任务的 PEIM 能够通过 PEI Services Table 调用 PEI Core 中的函数。
PEI Services Table 结构通常包含多个部分,其中最重要的就是核心服务 和辅助服务的指针:
- 核心服务 (Core Services): 最基础的服务,在整个 PEI 阶段都可用。
AllocatePages/AllocatePool: 内存分配。CreateHob: 创建 Hand-Off Block,将数据传递给 DXE。InstallPpi: 安装 PPI (PEIM-to-PEIM Interface),用于 PEIM 间通信。LocatePpi: 查找其他 PEIM 已经安装的 PPI。
- 启动服务 (Boot Services): 与启动流程管理相关的服务。
FfsFindNextPeim: 用于在 Firmware Volume (FV) 中查找下一个要执行的 PEIM。NotifyPpi: 注册一个通知函数,当某个 PPI 被安装时触发。
如上文中的伪代码所示,这个服务表在 PEI Core 启动时(在 SEC 阶段交接之后)被初始化,最初的实现可能非常精简,使用的都是临时内存(CAR/Cache As RAM)。当 PEI Core 加载并执行一个 PEIM 时,它会将 PEI Services Table 的地址作为参数传递给该 PEIM 的 EntryPoint 函数。伪代码如下,对应上文伪代码中的 41 行。
c
// PEIM Entry Point 函数的签名
Function PeimEntryPoint(
IN PeiFileHandle,
IN **PeiServicesTablePointer // 这里的双指针指向服务表
)
// PEIM 现在可以使用这个表来调用服务
Status = PeiServicesTablePointer->CreateHob(...)
Status = PeiServicesTablePointer->InstallPpi(...)
End Function
四、HOB的详细介绍,PEI 到 DXE阶段
HOB 的主要任务是将 PEI 阶段发现、初始化和配置的所有系统信息,以一种标准化的格式,安全可靠地传递给接下来运行的 DXE 阶段。PEI 阶段创建的 HOB 并不是一个单一的数据块,而是一个由多个独立 HOB 描述符 串联而成的链表,称为 HOB 列表 (HOB List)。
核心作用
- 状态传递: 记录内存、CPU、芯片组等硬件的状态和配置。
- 资源映射: 提供系统内存和 I/O 资源的详细布局,供 DXE 阶段的内存管理器和驱动程序使用。
- 服务注册: 记录 PEI 阶段可能提供的某些特殊服务或数据。
每个 HOB 都是由一个标准化的 HOB 描述符头部 (EFI_HOB_GENERIC_HEADER) 开始,紧接着是该 HOB 特定类型的数据。
| 字段 | 描述 |
|---|---|
Type |
HOB 的类型(见下文)。 |
Length |
整个 HOB 结构的长度(包括头部和数据)。 |
Reserved |
保留字段。 |
HOB 列表中的每一个块都属于一个特定的类型,用于传递特定的信息。以下是几种最关键的 HOB 类型:
| HOB 类型 (Type) | 结构名称 (例如) | 传递的信息 |
|---|---|---|
EFI_HOB_TYPE_RESOURCE_DESCRIPTOR |
EFI_HOB_RESOURCE_DESCRIPTOR |
内存和 I/O 资源的详细列表。 这是 DXE 内存管理器建立内存映射的基础。 |
EFI_HOB_TYPE_MEMORY_ALLOCATION |
EFI_HOB_MEMORY_ALLOCATION |
记录 PEI 阶段已分配的内存块(例如,用于存储 PEI 堆栈、PEI 核心、或特定的数据结构)。 |
EFI_HOB_TYPE_HANDOFF |
EFI_HOB_HANDOFF_INFO_TABLE |
列表的第一个 HOB。 包含 PEI 和 DXE 之间的手递手信息,例如:启动模式、SEC 阶段的堆栈基址等。 |
EFI_HOB_TYPE_CPU |
EFI_HOB_CPU |
CPU 特定的信息,例如:CPU 的最大地址空间、处理器数量等。 |
EFI_HOB_TYPE_GUID_EXTENSION |
EFI_HOB_GUID_TYPE |
允许平台或 OEM 定义自定义数据,通过 GUID 识别,用于传递非标准化的信息。 |
在 PEI 阶段,当一个 PEIM (PEI Module) 完成初始化或发现 某个资源时,它会调用 PEI 服务表中的 API 来创建 HOB,将其添加到 HOB 列表的末尾。
主要的 HOB 创建服务是:
-
PeiServicesCreateHob(): 用于创建任何类型的 HOB。 -
PeiServicesInstallPeiMemory(): 专用于创建第一个内存资源 HOB,标志着永久内存初始化完成。
HOB 如何从 PEI 传递到 DXE,这是 HOB 机制的核心价值:
- PEI 结束: PEI 核心在完成所有 PEIM 的执行后,会调用
PeiCoreExitTransition()函数。 - Handoff Block 准备: 在退出前,PEI 核心确保 HOB 列表已经最终化。
- 跳转到 DXE: PEI 核心最后会执行一个特殊的 PEIM,该 PEIM会加载 DXE 核心的映像,并使用特殊的跳转指令(通常是
JMP或CALL)进入 DXE 阶段的入口点 (DxeCoreEntry)。 - 传递参数: 在跳转时,HOB 列表的起始地址会被作为参数(通常通过 CPU 寄存器)传递给 DXE 核心。
DXE 阶段如何使用 HOB。DXE 核心是 HOB 列表的主要消费者
- 初始化 DXE 核心: DXE 核心的入口点接收到 HOB 列表的地址。它首先解析这个列表,建立其内存管理器。
- 建立内存映射: 它利用
EFI_HOB_TYPE_RESOURCE_DESCRIPTORHOB 中包含的内存信息,来建立自己的内存映射,区分可用内存和已占用内存。 - 提取配置信息: DXE 驱动和 DXE 服务可以使用
EfiGetSystemConfigurationTable()或直接遍历 HOB 列表来查找特定的配置信息或自定义数据。 - 销毁(逻辑上): 一旦 DXE 核心完成了初始化并建立了自己的资源管理机制,HOB 列表的历史使命就完成了。它所占据的内存通常会被重新声明 为 DXE 阶段可用的系统内存。
Steady Progress