UEFI-PEI 阶段的深层介绍

一、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 机制的核心价值:

  1. PEI 结束: PEI 核心在完成所有 PEIM 的执行后,会调用 PeiCoreExitTransition() 函数。
  2. Handoff Block 准备: 在退出前,PEI 核心确保 HOB 列表已经最终化。
  3. 跳转到 DXE: PEI 核心最后会执行一个特殊的 PEIM,该 PEIM会加载 DXE 核心的映像,并使用特殊的跳转指令(通常是 JMPCALL)进入 DXE 阶段的入口点 (DxeCoreEntry)。
  4. 传递参数: 在跳转时,HOB 列表的起始地址会被作为参数(通常通过 CPU 寄存器)传递给 DXE 核心。

DXE 阶段如何使用 HOB。DXE 核心是 HOB 列表的主要消费者

  1. 初始化 DXE 核心: DXE 核心的入口点接收到 HOB 列表的地址。它首先解析这个列表,建立其内存管理器
  2. 建立内存映射: 它利用 EFI_HOB_TYPE_RESOURCE_DESCRIPTOR HOB 中包含的内存信息,来建立自己的内存映射,区分可用内存和已占用内存。
  3. 提取配置信息: DXE 驱动和 DXE 服务可以使用 EfiGetSystemConfigurationTable() 或直接遍历 HOB 列表来查找特定的配置信息或自定义数据。
  4. 销毁(逻辑上): 一旦 DXE 核心完成了初始化并建立了自己的资源管理机制,HOB 列表的历史使命就完成了。它所占据的内存通常会被重新声明 为 DXE 阶段可用的系统内存。

Steady Progress

相关推荐
切糕师学AI7 小时前
SWD(Serial Wire Debug,串行线调试)
嵌入式·swd·stm-32
小䌨狗狗9 小时前
学习记录:RT-Thread 初始化机制
嵌入式·rtt-hread
大聪明-PLUS10 小时前
在 Linux 上使用实时调度策略运行应用程序
linux·嵌入式·arm·smarc
FreakStudio17 小时前
串口协议解析实战:以 R60ABD1 雷达为例,详解 MicroPython 驱动中数据与业务逻辑的分离设计
python·单片机·pycharm·嵌入式·面向对象·硬件·电子diy
大聪明-PLUS2 天前
Linux 系统中的 CPU。文章 2:平均负载
linux·嵌入式·arm·smarc
余生皆假期-2 天前
SPWM 与 SVPWM 电压利用率简谈
单片机·嵌入式
赋能大师兄3 天前
单片机/嵌入式修行之路
单片机·嵌入式
大聪明-PLUS3 天前
Linux 中的 CPU。文章 1. 利用率
linux·嵌入式·arm·smarc
飞凌嵌入式3 天前
【玩转多核异构】T153核心板RISC-V核的实时性应用解析
linux·嵌入式硬件·嵌入式·risc-v