ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

UEFI PEI阶段深度解析:PeiCore三次调用与内存管理机制

UEFI PEI阶段深度解析:PeiCore三次调用与内存管理机制 1. 从一次开机卡死说起PEI阶段到底在干什么很多人第一次接触UEFI固件开发都是从改个启动Logo或者加个开机画面这种小需求开始的结果一头扎进代码里才发现真正让人抓狂的不是DXE阶段那些琳琅满目的Protocol而是SEC和PEI这两个看起来没几行代码的阶段。我自己当年调一块板子现象是插上内存条后串口只打印到PEI Core Entry就没了下文连DXE的影子都没见到。查了整整两天最后发现是PEI阶段临时内存的堆栈切换时机和内存初始化顺序对不上。这件事让我意识到PEI阶段虽然代码量不大但它是整个固件启动链条里最脆弱的一环——此时DRAM还没完全可用能用的资源极其有限任何一步顺序错了都会直接卡死而且往往没有任何有效报错。UEFI的启动流程大致分为SEC、PEI、DXE、BDS、TSL、RT、AL几个阶段。SEC阶段负责最底层的CPU初始化和建立临时环境做完之后把控制权交给PEI。PEI的全称是Pre-EFI Initialization翻译过来就是EFI初始化之前它的核心任务只有两件一是把内存DRAM初始化好二是为后面庞大的DXE阶段准备好运行环境。听起来简单但难点在于——在内存初始化完成之前代码本身也需要内存来跑这就形成了一个先有鸡还是先有蛋的问题。PeiCore作为PEI阶段的核心调度器它的三次调用机制和内存管理策略正是为了解决这个矛盾而设计的。这篇文章适合谁看如果你正在做BIOS/UEFI固件开发、在做国产平台或服务器主板的bring-up、或者单纯想搞明白电脑按下电源键之后那几秒钟到底发生了什么那这篇内容会对你有帮助。我会尽量把PeiCore的三次调用讲透把内存管理里那些容易踩坑的地方掰开揉碎包括堆栈切换的时机、临时内存和永久内存的交接、HOB列表的传递逻辑。文中涉及的具体实现细节我会基于业界常见的EDK II框架实践来补充说明不同平台可能有差异但核心思路是通用的。2. PeiCore的三次调用一个精心设计的三段式启动2.1 为什么PeiCore要被调用三次刚接触PEI代码的人看PeiCore()这个函数第一反应往往是困惑为什么它会被进入三次这不是重复执行吗其实这三次调用对应的是PEI阶段三个完全不同的生命周期节点每次进入时系统的资源状态都不一样做的事情也完全不同。你可以把它理解成装修房子第一次是毛坯房刚拿到钥匙先搭个临时工棚住着第二次是水电改造完成可以正式施工了第三次是硬装结束把工棚拆掉正式入住。这个设计的根本原因在于内存。PEI阶段开始时DRAM控制器还没配置物理内存不可用CPU只能访问Cache-as-RAMCAR也叫NEMNo-Eviction Mode或者芯片内部的SRAM。这些临时存储空间非常小通常只有几十KB到几百KB根本装不下完整的PEI调度逻辑和所有PEIM模块。所以PeiCore必须先用极简的方式跑起来等内存初始化完成后再搬家到真正的DRAM里用完整的逻辑重新跑一遍。三次调用的划分是这样的第一次调用发生在SEC刚交接过来、临时内存CAR可用但DRAM不可用时此时PeiCore做的是最基础的初始化建立临时的堆栈和HOB列表然后调度那些必须在临时内存里执行的PEIM比如内存初始化PEIM本身。第二次调用发生在内存初始化PEIM执行完、DRAM已经可用之后此时PeiCore把自己和堆栈迁移到DRAM重新调度所有PEIM这次是完整的调度。第三次调用则是在PEI阶段即将结束、准备切换到DXE之前做最后的清理和状态切换。2.2 第一次调用在螺蛳壳里做道场第一次进入PeiCore时系统处于最原始的状态。此时能用的内存就是CAR它的本质是把CPU的Cache配置成一种特殊模式让读写Cache的行为表现得像读写普通RAM但数据不会被换出到主存。CAR的大小取决于CPU型号和配置一般x86平台上常见的是128KB到512KB不等。PeiCore第一次调用时做的事情可以概括为几个关键步骤。首先是建立临时的堆Heap和栈Stack这个堆栈就放在CAR里。堆栈的大小需要仔细权衡太小了后面PEIM执行时可能溢出太大了CAR本来就不够用。EDK II里通常通过PcdPeiTemporaryRamBase和PcdPeiTemporaryRamSize这两个PCD来配置实际项目中我见过把临时RAM配成64KB的也见过配成256KB的取决于平台。然后是初始化HOBHand-Off Block列表。HOB是PEI阶段向DXE阶段传递信息的主要载体它是一个单向链表每个节点描述一段系统信息比如内存资源描述、可用内存范围、固件卷信息等等。第一次调用时会创建最基础的几个HOB包括PHIT_HOB描述临时内存和启动模式和FV_HOB描述固件卷位置。接下来是调度PEIM。这里有个关键点第一次调用时PeiCore只会调度那些被标记为在临时内存中执行的PEIM最典型的就是内存初始化PEIM比如MRC代码。其他PEIM会被暂时跳过因为它们的代码和数据可能放不进CAR。这个调度逻辑依赖PEIM的依赖关系Dependency和调度属性PeiCore通过PeiCoreDispatcher来管理。注意第一次调用时如果CAR配置不当最常见的现象就是跑着跑着就飞了串口打印突然乱码或者直接卡死。排查时优先检查CAR大小是否足够以及堆栈是否溢出。我一般会在堆栈顶部和底部各放一个魔术字Magic Number调度结束后检查这两个字有没有被改写这是最直接的溢出检测手段。2.3 第二次调用搬到大房子里重新来过内存初始化PEIM执行成功后DRAM已经可用此时PeiCore会进行第二次调用。这次调用的核心动作是迁移——把整个PEI的运行环境从CAR搬到DRAM。迁移过程有几个细节值得展开。首先是堆栈切换PeiCore会把当前的堆和栈内容从CAR复制到DRAM中的新位置然后切换栈指针Stack Pointer到新栈。这个切换必须非常小心因为切换的瞬间代码还在CAR里执行但栈已经指向DRAM了。如果切换顺序错了比如先释放了CAR再切栈那就会直接崩溃。正确的做法是先在DRAM里分配好新堆栈复制内容然后修改栈指针最后再标记CAR为可回收。其次是HOB列表的迁移。第一次调用时创建的HOB列表也在CAR里需要一并复制到DRAM。复制完成后PeiCore会更新HOB列表的指针后续所有对HOB的访问都指向DRAM中的新副本。迁移完成后PeiCore会重新遍历所有PEIM这次是完整调度。之前因为空间限制被跳过的PEIM现在都可以正常执行了。这些PEIM可能包括各种硬件初始化、策略配置、设备枚举等。第二次调度是PEI阶段真正干活的阶段大部分PEIM都在这个时候执行。这里有个容易混淆的点第二次调用时PeiCore本身也是从DRAM里执行的。也就是说PeiCore的代码在第一次调用时是从CAR或者只读的Flash映射执行的第二次调用时已经迁移到了DRAM。这个迁移过程对PEIM是透明的PEIM不需要关心自己在哪次调用中被执行只需要按照标准接口实现PeimEntryPoint即可。2.4 第三次调用交接前的最后整理第三次调用发生在所有PEIM都调度完成、PEI阶段准备切换到DXE之前。这次调用做的事情相对轻量但同样关键。主要工作包括完成HOB列表的最终整理把所有需要传递给DXE的信息都打包好关闭PEI阶段使用的临时资源比如CAR如果还没关闭的话设置好切换到DXE所需的上下文包括入口点地址、栈指针、HOB列表指针等。最后PeiCore会调用SwitchStack或者类似的平台相关函数把控制权交给DXE Core。第三次调用时有一个重要的检查确保所有必须的HOB都已经创建。DXE阶段依赖HOB来了解系统资源如果某个关键HOB缺失DXE可能会跑飞或者枚举不到设备。常见的必须HOB包括内存资源HOB、固件卷HOB、CPU信息HOB等。我在实际项目中遇到过因为漏了一个内存描述HOB导致DXE阶段只认到一半内存的情况排查起来相当费劲。3. 内存管理机制从CAR到DRAM的惊险一跃3.1 临时内存CAR的配置与陷阱CAR是PEI阶段最核心的资源它的配置直接决定了PEI能不能跑起来。CAR的实现依赖CPU的Cache控制器通过设置特定的MSRModel Specific Register把一部分Cache配置成锁定模式这部分Cache不再参与正常的缓存替换而是像SRAM一样被读写。配置CAR时需要考虑几个参数。一是CAR的基地址和大小这通常由芯片组决定比如某些平台CAR固定在0xFEF00000附近大小可配。二是Cache的Way数量CAR通常只使用部分Cache Way剩下的Way还可以作为正常缓存使用。三是CAR的初始化时机必须在任何使用内存的代码之前完成通常在SEC阶段就做好了。实际项目中最容易踩的坑是CAR大小不够。PEIM的代码和数据、堆、栈、HOB列表都要放在CAR里如果PEIM比较多或者某个PEIM的静态数据很大CAR很容易被撑爆。我一般的做法是先按保守估计配一个较大的CAR比如256KB等PEI跑通后再逐步缩小找到实际需要的最小值。这样虽然浪费了一点Cache但能避免很多莫名其妙的崩溃。另一个坑是CAR的不可重入特性。CAR本质上是Cache如果代码在CAR里执行时发生了Cache替换比如访问了CAR范围之外的地址导致Cache Line被换出CAR里的数据就可能丢失。所以PEI阶段早期必须避免访问CAR范围之外的内存所有数据都要放在CAR里。这个约束在写PEIM时要特别注意尤其是那些从Flash直接执行的PEIMXIPeXecute In Place它们的代码在Flash里但数据必须在CAR里。3.2 堆栈切换的时机与实现细节堆栈切换是PEI阶段最惊险的操作之一切换时机和实现方式直接关系到系统稳定性。切换的时机是在内存初始化PEIM成功返回之后、第二次调用PeiCore之前。此时DRAM已经可用但PeiCore还在用CAR里的堆栈。切换过程大致如下首先在DRAM中分配一块区域作为新堆栈大小通常比CAR堆栈大得多比如1MB以上然后把CAR堆栈的内容复制到新堆栈接着修改栈指针寄存器x86上是RSP指向新堆栈的对应位置最后把CAR标记为可回收后续不再使用。这个过程中最微妙的是复制内容这一步。栈里存的是函数调用链、局部变量、返回地址等复制时必须保证相对位置不变。也就是说如果旧栈指针是0x1000新栈基址是0x2000那么旧栈里偏移0x100的内容要复制到新栈的0x2100。这样复制完成后把RSP从0x1000改成0x2000栈上的所有数据就平移过去了函数返回时还能找到正确的返回地址。提示堆栈切换的代码通常用汇编实现因为C语言无法直接控制栈指针。EDK II里可以参考PeiSwitchStacks或者平台相关的SwitchStack实现。切换前后要关中断避免中断处理程序用到旧栈。我在实际调试中遇到过一个经典问题堆栈切换后某个PEIM的全局变量值变了。查了半天发现是那个PEIM的全局变量被放在了CAR里切换后CAR被回收变量就丢了。解决办法是把这类全局变量显式放到DRAM里或者改成从HOB里读取。这个坑提醒我们PEI阶段的内存布局不是透明的开发者必须清楚每一块数据在哪里。3.3 HOB列表PEI向DXE传递信息的接力棒HOBHand-Off Block是PEI阶段最重要的数据结构之一它承载了从PEI到DXE的所有关键信息。理解HOB的组织方式和传递机制是掌握PEI内存管理的关键。HOB列表是一个单向链表每个HOB有一个统一的头部EFI_HOB_GENERIC_HEADER包含HobType、HobLength和Reserved字段。HobType决定了这个HOB的具体结构比如EFI_HOB_TYPE_MEMORY_ALLOCATION表示内存分配信息EFI_HOB_TYPE_RESOURCE_DESCRIPTOR表示资源描述EFI_HOB_TYPE_GUID_EXTENSION表示厂商自定义信息。HOB的创建通过BuildGuidHob、BuildResourceDescriptorHob、BuildMemoryAllocationHob等函数完成。这些函数会在当前可用的内存早期是CAR后期是DRAM中分配空间初始化HOB内容然后把它挂到HOB列表的末尾。HOB列表的头部指针通常保存在一个固定的位置比如gHobList全局变量或者通过PcdPeiCoreHobList传递。HOB列表在堆栈切换时也要迁移。第一次调用时创建的HOB在CAR里第二次调用时要复制到DRAM。复制过程是逐个节点复制然后重建链表关系。复制完成后所有后续的HOB操作都在DRAM里进行。这里有个细节HOB列表是只增不减的。PEI阶段创建的HOB在DXE阶段仍然可以访问DXE可以遍历HOB列表获取信息但不能修改或删除HOB。这个设计保证了信息的完整性和一致性。DXE阶段如果需要传递信息给更后面的阶段会使用其他机制比如UEFI变量、Configuration Table等。3.4 内存分配策略PEI阶段的精打细算PEI阶段的内存分配和DXE阶段完全不同。DXE阶段有完整的内存管理服务如AllocatePool、AllocatePages可以灵活分配和释放内存。PEI阶段则非常原始只有几种简单的分配方式。第一种是PeiServicesAllocatePool它从PEI的堆里分配内存。这个堆在第一次调用时位于CAR第二次调用后迁移到DRAM。堆的大小有限分配后不能释放PEI阶段没有FreePool所以必须精打细算。第二种是PeiServicesAllocatePages它从系统内存中分配页。这个函数在内存初始化完成后才可用分配的是物理页通常用于需要较大块内存的场景。第三种是HOB分配前面提到的BuildGuidHob等函数本质上也是内存分配只不过分配出来的内存会被组织成HOB生命周期一直延续到DXE。PEI阶段内存分配的核心原则是够用就好绝不浪费。因为早期内存CAR极其有限每一个字节都要省着用。我见过有工程师在PEIM里定义一个几KB的全局数组结果直接把CAR撑爆了。正确的做法是能用局部变量就不用全局变量能用小结构就不用大结构必须用大内存时优先用AllocatePages从DRAM分配。4. 实操过程从零搭建一个可调试的PEI环境4.1 环境准备与代码框架搭建要实际动手研究PeiCore最直接的方式是基于EDK II搭建一个可编译、可调试的固件工程。EDK II是业界最主流的UEFI固件开发框架虽然学习曲线陡峭但资料相对丰富社区也活跃。环境准备大致需要以下几步。首先是安装编译工具链Linux下通常用GCCWindows下可以用VS或者GCC。然后是获取EDK II源码可以通过git克隆官方仓库。接着是配置构建环境设置EDK_TOOLS_PATH、PACKAGES_PATH等环境变量运行edksetup.shLinux或edksetup.batWindows初始化。代码框架方面一个最小的PEI工程需要几个关键组件一个平台描述文件.dsc、一个组件描述文件.inf、以及PeiCore和各个PEIM的源码。EDK II提供了MdeModulePkg/Core/Pei作为PeiCore的参考实现MdeModulePkg/Universal/...下有各种通用PEIM平台相关的PEIM通常放在PlatformPkg里。编译命令一般是build -a X64 -t GCC5 -p PlatformPkg/PlatformPkg.dsc具体参数根据平台调整。编译产物是一个FDFirmware Device文件可以烧录到Flash或者用模拟器加载。注意初次搭建EDK II环境时最容易卡在工具链配置上。建议先用官方提供的OvmfPkg开源虚拟机固件跑通编译流程确认工具链没问题后再换成自己的平台。OvmfPkg的好处是它可以在QEMU里直接运行调试非常方便。4.2 关键调试手段串口打印与源码级调试PEI阶段的调试手段比DXE阶段少得多因为此时大部分调试设施还没建立起来。最常用的手段是串口打印通过DEBUG宏或者SerialPortWrite直接往串口输出信息。串口打印在PEI早期就能用前提是串口硬件已经初始化。通常会在SEC阶段或者PEI最开始就初始化串口这样后续所有阶段都能打印。打印内容要精简因为串口速度有限打印太多会拖慢启动速度。我一般会在关键节点打印比如PeiCore Entry、Memory Init Start、Memory Init Done、Stack Switch Done、PeiCore Exit等。源码级调试需要硬件调试器如JTAG、ITP、DSTREAM等配合。通过调试器可以设置断点、单步执行、查看寄存器和内存。PEI阶段的调试难点在于符号信息因为此时还没有加载符号表需要手动指定符号文件或者用地址映射。EDK II编译时会生成.map文件和调试信息配合调试器可以定位到具体函数。还有一种调试手段是点灯——通过GPIO控制LED闪烁来指示执行进度。这在串口不可用或者串口初始化有问题时特别有用。比如在PeiCore入口点亮LED1内存初始化完成后点亮LED2堆栈切换后点亮LED3通过LED的状态就能判断卡在哪一步。4.3 内存初始化PEIM的编写要点内存初始化PEIM是PEI阶段最核心的PEIM它负责配置DRAM控制器、训练内存参数、报告可用内存范围。这个PEIM通常由芯片厂商提供比如Intel的MRC、AMD的MPRC平台开发者一般不需要从头写但需要理解它的接口和调用方式。内存初始化PEIM的入口点遵循标准PEIM接口通过PeimEntryPoint注册。它需要读取平台配置信息比如SPD数据、内存频率、时序参数配置内存控制器寄存器执行内存训练最后通过BuildResourceDescriptorHob和BuildMemoryAllocationHob报告内存资源。编写或集成内存初始化PEIM时有几个要点。一是依赖关系内存初始化PEIM必须在其他需要内存的PEIM之前执行这通过PEIM的DEPEX依赖表达式来保证。二是错误处理内存训练可能失败PEIM需要检测失败并采取相应措施比如降频重试、报错退出。三是性能内存训练比较耗时要尽量优化避免开机时间过长。我在实际项目中遇到过内存训练失败导致开机黑屏的情况。排查时发现是SPD数据读取错误某个内存条的SPD被误读成了不支持的频率。解决办法是在内存初始化PEIM里增加SPD校验对异常数据做容错处理。这个经验说明内存初始化PEIM不仅要能跑还要跑得稳对各种异常情况都要有处理。4.4 堆栈切换的代码实现与验证堆栈切换的代码实现是PEI阶段的技术难点之一。下面给出一个简化的实现思路基于x86_64架构。首先定义一个结构体来描述堆栈切换所需的信息typedef struct { UINTN OldStackBase; UINTN OldStackSize; UINTN NewStackBase; UINTN NewStackSize; UINTN NewStackTop; } PEI_STACK_SWITCH_CONTEXT;切换函数用汇编实现核心逻辑是计算新旧栈的偏移复制栈内容然后修改RSP; 假设 rdi 新栈顶, rsi 旧栈顶, rdx 栈大小 PeiSwitchStack: mov rcx, rdx ; rcx 栈大小 mov r8, rdi ; r8 新栈顶 mov r9, rsi ; r9 旧栈顶 sub r8, rcx ; r8 新栈底 sub r9, rcx ; r9 旧栈底 ; 复制栈内容 cld rep movsb ; 从 rsi 复制到 rdi长度 rcx ; 切换栈指针 mov rsp, rdi ; rsp 指向新栈顶 ret实际实现中还要考虑对齐、中断状态、寄存器保存等细节。EDK II的MdeModulePkg/Core/Pei/Dispatcher/Dispatcher.c里有完整的参考实现可以对照学习。验证堆栈切换是否成功可以在切换前后打印栈指针的值确认RSP确实变了。还可以在切换后调用一个函数检查它的局部变量是否正常以此验证新栈可用。我通常会在切换后立即执行一次DEBUG打印如果打印正常说明栈切换基本成功。5. 常见问题与排查技巧实录5.1 PEI阶段卡死的典型现象与定位方法PEI阶段卡死是最常见的问题现象多种多样串口无输出、串口输出到某一句就停、串口输出乱码、反复重启等。定位这类问题需要系统性的方法。第一步是确认卡死的大致位置。如果串口完全无输出问题可能在SEC阶段或者PeiCore入口之前如果输出到某一句就停问题就在那一句之后。串口打印是最重要的线索所以PEI阶段的打印要足够密集覆盖所有关键节点。第二步是检查CAR配置。CAR大小不够、基地址冲突、Cache Way配置错误都会导致早期崩溃。可以用调试器查看CAR区域的读写是否正常或者临时增大CAR看问题是否消失。第三步是检查堆栈。堆栈溢出是PEI阶段的高频问题尤其是在CAR里堆栈很小的情况下。可以在堆栈边界放魔术字调度结束后检查是否被改写。也可以用调试器监控RSP的变化看是否超出了堆栈范围。第四步是检查HOB列表。HOB列表损坏会导致后续阶段崩溃但现象可能延迟出现。可以在每次创建HOB后遍历列表确认链表完整、节点内容正确。下面这张表整理了我遇到过的典型现象和对应的排查方向现象可能原因排查方向串口完全无输出SEC阶段问题、CAR未初始化、串口未初始化检查SEC代码、CAR配置、串口初始化输出到PeiCore Entry停止CAR不足、PeiCore初始化失败增大CAR、检查PeiCore入口代码输出到Memory Init停止内存初始化PEIM失败检查SPD、内存控制器配置、训练参数堆栈切换后崩溃栈复制错误、栈指针未更新、CAR提前回收检查切换代码、验证新栈可用性DXE阶段枚举不到设备HOB缺失或损坏遍历HOB列表、检查关键HOB是否创建反复重启看门狗未处理、异常未捕获检查看门狗配置、异常处理代码5.2 内存相关问题的独家避坑技巧内存问题是PEI阶段最头疼的一类问题因为此时没有完整的内存管理服务很多问题只能靠经验和工具定位。以下是我在实际项目中总结的几个技巧。第一个技巧是内存填充法。在PEI早期把CAR和DRAM的关键区域填充成特定模式比如0xAA然后在关键节点检查这些模式是否被改写。如果某个区域被改写了说明有代码越界访问。这个方法对定位缓冲区溢出特别有效。第二个技巧是影子变量法。对于关键变量在内存中保存两份定期比对。如果两份不一致说明有代码意外修改了变量。这个方法对定位野指针、内存踩踏很有用。第三个技巧是分阶段验证法。把PEI阶段分成若干小段每段结束后做一次完整性检查检查堆栈、HOB、关键数据结构。一旦发现问题就能快速定位到是哪一段引入的。这个方法虽然增加了代码量但能大幅缩短调试时间。第四个技巧是最小系统法。当问题复杂难以定位时把PEIM裁剪到最少只保留最核心的几个看问题是否还存在。如果问题消失再逐步加回PEIM直到问题复现就能定位到是哪个PEIM引入的。提示PEI阶段的内存问题往往有延迟性即问题在PEI阶段引入但现象在DXE阶段才出现。所以排查DXE问题时也要回头检查PEI阶段的HOB和内存布局。5.3 从PEI到DXE的交接检查清单PEI阶段结束、切换到DXE之前有一系列检查必须做否则DXE阶段可能出各种问题。我整理了一份检查清单每次bring-up新平台时都会对照检查。HOB列表完整性遍历所有HOB确认链表无断裂、节点长度正确、类型合法。内存资源HOB确认所有可用内存范围都被正确报告没有遗漏或重叠。固件卷HOB确认所有固件卷的位置和大小正确DXE能正确加载驱动。CPU信息HOB确认CPU核心数、特性、频率等信息正确。栈指针确认切换到DXE时栈指针指向有效的DRAM区域且栈空间足够。入口点确认DXE Core的入口点地址正确且代码已加载到内存。中断状态确认中断已关闭避免切换过程中被中断打断。看门狗确认看门狗已处理避免切换后立即复位。这份清单看起来简单但每一条都对应着实际项目中踩过的坑。比如内存资源HOB遗漏会导致DXE只认到部分内存固件卷HOB错误会导致DXE加载驱动失败栈指针问题会导致DXE一启动就崩溃。把这些检查做在前面能省下大量调试时间。5.4 性能优化让PEI阶段跑得更快PEI阶段的性能直接影响开机速度尤其是在服务器和嵌入式场景下开机时间是很敏感的指标。优化PEI阶段的性能可以从几个方面入手。一是减少PEIM数量。每个PEIM都有调度开销不必要的PEIM应该裁掉。EDK II支持通过PCD或者DSC配置来裁剪PEIM把不需要的功能去掉。二是优化内存初始化。内存训练是PEI阶段最耗时的部分可以通过缓存训练结果、并行训练多个通道、使用更快的训练算法来加速。有些平台支持快速启动模式跳过部分训练步骤用上次的结果。三是减少串口打印。串口打印虽然对调试有用但会显著拖慢启动速度。量产固件里应该把调试打印关掉或者降到最低级别。四是优化HOB操作。HOB列表是链表频繁的遍历和插入会影响性能。可以通过缓存常用HOB的指针、减少不必要的HOB创建来优化。五是并行化。多核平台上部分PEIM可以并行执行比如不同内存通道的初始化。EDK II支持MPMulti-ProcessorPEIM可以利用多核加速。我在一个服务器项目里做过统计PEI阶段占了整个开机时间的30%左右其中内存初始化又占了PEI阶段的70%。通过优化内存训练算法和并行化把PEI阶段的时间缩短了将近一半。这个收益在批量部署的场景下非常可观。6. 我个人的一些经验体会PEI阶段是UEFI固件里最硬核的部分它离硬件最近抽象最少调试手段最有限。但也正因为如此把PEI阶段搞透之后对整个固件启动流程的理解会上一个台阶。我自己的经验是不要一上来就啃PeiCore的源码那样很容易迷失在细节里。更好的路径是先理解PEI阶段要解决的核心问题内存初始化、环境准备、信息传递然后带着问题去看代码看PeiCore是怎么用三次调用和HOB机制解决这些问题的。另外PEI阶段的很多问题没有标准答案不同平台、不同芯片组的实现差异很大。遇到问题时除了查文档和代码多和芯片厂商的FAE沟通多参考同行的经验往往能少走很多弯路。我当年那个CAR撑爆的问题就是在一个论坛上看到别人提到类似现象才找到方向的。最后分享一个小技巧在PEI阶段的关键路径上多埋一些检查点每个检查点做一次轻量的完整性检查比如检查栈魔术字、遍历HOB列表。这些检查在量产固件里可以关掉但在bring-up阶段能帮你快速定位问题。等平台稳定后再逐步移除这些检查优化性能。这个习惯让我在很多项目里省下了大量调试时间希望对你有帮助。
返回列表