在关键的PEI和DXE阶段加载所有服务协议以及对硬件进行支持的驱动

 

目录

一.PEI 简介... 2

二.名词解释... 2

三.PEI阶段执行顺序... 3

四.PEIM分析... 4

DXE阶段主函数流程... 

阶段转换.... 

HOB详解.... 

调度器.... 

两种驱动类型.... 

 

一.PEI 简介

PEI的全称是Pre EFI Initialization,在UEFI启动过程中主要完成以下任务

1. 芯片组初始化

2. 内存初始化

3. UEFI环境初始化

5. 将代码运行环境切换到内存 (取消 CAR,重新启用缓存)

6. 启动DXEIPL(DXE Initial Program Loader)

执行流程如下图所示:

 

二.名词解释

1.PEI Core:PEI内核,负责提供PEI阶段的基础服务和执行流程,存储在BFV区域。

2.PEIM(Pre-EFI Initialize Module):独立的模块,每一个都负责一项具体的初始化工作。存储在FVs区域。

3.PPI:PEIM to PEIM Interface,每个PEIM中都包含的一个结构体,有函数指针和GUID,可以让一个PEIM调用另一个PEIM。

4.PEI Dispatcher:PEI Core的一部分,用来寻找BFV中存储的PEIM并启动它们。

5.PEI Service:PEI Core提供给所有PEIM使用的基础服务。

6.HOB:Hand-off block,是PEI阶段向DXE传递系统信息的手段。在PEI阶段构建一些HOB结构来存放系统状态数据,然后将其作为参数传给DXE入口函数,DXE Core会根据HOB列表来初始化UEFI系统服务。在HOB List中的第一个HOB必须是PHIT HOB(Phase Handoff Information Table),最后一个HOB必须是End of HOB List HOB。中间的HOB列表用来存放信息。HOB在PEI到DXE传送信息的过程遵循one Producer to one Consumer的模式,即在PEI阶段,一个PEIM创建一个HOB,在DXE阶段,一个DXE Driver使用那个HOB并且把HOB相关的信息传送给其他的需要这些信息的DXE组件。

 

三.PEI阶段执行顺序

下文中的代码均来自于EDKⅡ UDK2018,以OvmfPkg包编译的固件为例。

SEC阶段的最后一项工作就是跳转到PEI入口。在FD固件镜像中的FV_PEIFV区域,存放了以下内容:

INF  MdeModulePkg/Core/Pei/PeiMain.inf

INF  MdeModulePkg/Universal/PCD/Pei/Pcd.inf

INF  MdeModulePkg/Universal/ReportStatusCodeRouter/Pei/ReportStatusCodeRouterPei.inf

INF  MdeModulePkg/Universal/StatusCodeHandler/Pei/StatusCodeHandlerPei.inf

INF  OvmfPkg/PlatformPei/PlatformPei.inf

INF  MdeModulePkg/Core/DxeIplPeim/DxeIpl.inf

INF  UefiCpuPkg/Universal/Acpi/S3Resume2Pei/S3Resume2Pei.inf

!if $(SMM_REQUIRE) == TRUE

INF  OvmfPkg/SmmAccess/SmmAccessPei.inf

!endif

INF  UefiCpuPkg/CpuMpPei/CpuMpPei.inf

 

每个inf文件代表一个module,以上module一起完成UEFI在PEI阶段的工作。

我们首先来看PeiMain.inf文件的首段:

[Defines]

  INF_VERSION                    = 0x00010005

  BASE_NAME                      = PeiCore

  MODULE_UNI_FILE                = PeiCore.uni

  FILE_GUID                      = 52C05B14-0B98-496c-BC3B-04B50211D680

  MODULE_TYPE                    = PEI_CORE

  VERSION_STRING                 = 1.0

  ENTRY_POINT                    = PeiCore

这一段定义了PeiMain这个module的名称和入口是PeiCore,也就是说SEC阶段寻找PEI阶段入口时,会找到这一模块的地址,然后运行[Sources]中设置的代码。

入口段代码PeiMain.c的工作流程如下:

  1. 声明EFI_PEI_SERVICES(EFI服务列表),包括安装、卸载、定位PPI、操作CPU缓存和内存等服务,这些服务是PEI阶段顺利执行的基础。
  2. 进入PeiCore运行阶段1,检索SEC阶段传递过来的SecCoreData 的内容并存储,这是一个包含了PEI阶段运行环境信息的数据结构,比如当前使用的临时内存的大小和位置,栈和BFV固件的地址等,存入SecCoreData。
  3. 判断OldCoreData是否为空,若为空则表明这是在内存初始化前进入PEI Core。
  4. 获取指向PEI服务列表的指针。初始化PEI服务,使其常驻内存(此时为临时内存),可以被随时调用。
  5. 如果有SEC阶段传递的PPI列表,则进行处理。
  6. 启动PeiDispatcher,开始进行硬件资源的初始化。

(以下流程代码在MdeModulePkg\Core\Pei\Dispatcher目录中的Dispatcher.c文件中)

  1. 主要的调度循环会在已知的FV区域中搜索PEIM,并尝试调度它们。 如果有任何PEIM被成功调度并完成了该部分工作,就会在OldCoreData->HobList中添加一个HOB,然后此循环会从Bfv重新开始搜索,从而确定是否存在运行条件已被满足的新的PEIM。 如果FV中的PEIM存放顺序完美遵循了依赖关系,每个PEIM被发现时都满足运行条件,则该循环就只会运行一次。
  2. 在每次PEIM成功运行时,调用PeiCheckAndSwitchStack来检查内存是否已被初始化,若是,则进行栈切换,流程如下:

a)   在将堆栈从临时内存切换到永久内存之前,计算临时内存中的堆和堆栈使用情况以输出调试用的信息。

b)   为PEI代码预分配内存范围。

c)   在物理内存的底部保留新堆栈的空间,此空间不小于临时内存中堆栈的大小。

d)   分别计算临时内存和新永久内存之间的栈偏移量和堆偏移量

e)   构建保存永久内存堆栈信息的HOB。

f)   缓存SecCoreData中的信息,以免在栈切换过程中丢失。

g)   计算永久内存中栈内的HandOffTable和PrivateData新地址。

h)   调用TemporaryRamSupportPpi的TemporaryRamMigration函数,它将临时内存复制到永久内存并切换运行环境。调用该函数之后,代码运行使用的堆栈都位于永久内存中。

i)   调用MigrateMemoryPages将之前分配的内存也迁移到永久内存中。

j)   重新启动PeiCore。

(返回到PeiMain.c的代码中)

  1. 判断OldCoreData是否为空,不为空,表明这是在内存初始化后进入PEI Core。

10. 读取PeiCore模块代码并存入永久内存中。

11. 获取指向PEI服务列表的指针。初始化PEI服务,使其常驻内存(此时为永久内存),可以被随时调用。

12. 再次启动PeiDispatcher,调度剩余的PEIM进行初始化工作。完成后返回结束标记。

13. 调用PeiServicesLocatePpi来定位DXE IPL PPI,此PPI用来获取DXE阶段入口地址。如果没有找到则报告EFI_ERROR_CODE和EFI_SW_PEI_CORE_EC_DXEIPL_NOT_FOUND,并调用CpuDeadLoop函数,启动过程停止。

14. 进入DXE入口并传递HOB列表。

15. PEI阶段结束。

 

四.PEIM分析

每个PEIM都是独立的模块,并且可能分布在固件中的不同位置。为了便于被定位和调度,具有统一定义的入口函数,如下所示:

EFI_STATUS

EFIAPI

_ModuleEntryPoint (

  IN EFI_PEI_FILE_HANDLE       FileHandle,

  IN CONST EFI_PEI_SERVICES    **PeiServices

  )

{

  if (_gPeimRevision != 0) {

    // 确保当前运行的UEFI所遵循的PEI标准版本不小于被启用的驱动所遵循的PEI标准版本

    ASSERT ((*PeiServices)->Hdr.Revision >= _gPeimRevision);

  }

  ProcessLibraryConstructorList (FileHandle, PeiServices);

  return ProcessModuleEntryPointList (FileHandle, PeiServices);

}

每个模块被装载到内存后的image具有入口函数_ModuleEntryPoint,严格来说PEI Core就是最先启动的PEIM,通过PPI来调度其他的PEIM。在PEIM Dispatcher寻找可启动的PEIM时,会先在每一个FV上定位Apriori文件,然后读取文件内容来查找PEIM的GUID,确保Apriori文件中的PEIM被首先调用。在OvmfPkgX64.fdf中我们可以看到FV.PEIFV中有以下内容:

APRIORI PEI {

  INF  MdeModulePkg/Universal/PCD/Pei/Pcd.inf

}

说明PCD模块就是PEI Dispatcher首先启动的PEIM。然后才对FV.PEIFV中定义的PEI 阶段的其他模块进行调用。最终调用DXE IPL的PPI来启动DXE,此时DXE也类似于一个PEIM。

在PEI阶段,最重要的工作是CPU、Platform和Memory的初始化。cpu的入口inf文件定义如下:

[Defines]

  INF_VERSION                    = 0x00010005

  BASE_NAME                      = CpuPei

  MODULE_UNI_FILE                = CpuPei.uni

  FILE_GUID                      = 01359D99-9446-456d-ADA4-50A711C03ADA

  MODULE_TYPE                    = PEIM

  VERSION_STRING                 = 1.0

  ENTRY_POINT                    = CpuPeimInit

Patform初始化包含大量不同的PEIM,互相之间存在依赖关系,需要最先运行的是PlatformEarlyInit,其入口inf文件定义如下:

[Defines]

  INF_VERSION                    = 0x00010005

  BASE_NAME                      = PlatformEarlyInit

  FILE_GUID                      = EE685731-CFF3-4ee7-9388-7E63FC5A59B0

  MODULE_TYPE                    = PEIM

  ENTRY_POINT                    = PlatformEarlyInitEntry

编译时包含的源代码文件如下:

[Sources]

  PchInitPeim.c

  Common/FlashMap.c

  Common/Stall.c

  MemoryPeim.c

  MemoryCallback.c

  PlatformEarlyInit.c

  Recovery.c

  SioInitPeim.c

 

其中包含了对内存的初始化PEIM。

DXE阶段主函数流程

1)      DXE阶段的源码位于MdeModulePkg/Core/Dxe/DxeMain/目录中。从DxeMain.inf文件中我们可以找到该阶段使用所有源码、依赖库、protocols和PCD的列表。

2)      DXE Core的主函数DxeMain执行过程如下:

3)      把HOB列表的指针转换为union格式,便于使用。

4)      初始化内存服务。

5)      根据EFI System Table和EFI Runtime Service Table这两张表中的内容初始化EFI相关服务。

6)      AMI代码在此处对AmiLib进行了初始化,从而保证能够运行AmiDxeLib模块中的函数。

7)      根据HOB列表的内容对其他系统服务进行初cmd始化,如映像服务,事件服务等。

8)      获取所有的protocol,注册其GUID。protocol是一种特殊的结构体,用于驱动之间的通信。

9)      初始化DXE Dispatcher并启动。

10)   当一个FV的protocol被安装时,该FV中的每个驱动程序都会被添加到mDiscoveredList。如果FV中存在Apriori文件,则其中包含的驱动程序会被引导到mScheduledQueue。

11)   DXE Dispatcher从mScheduledQueue中依次加载驱动程序并启动它。在mScheduledQueue被清空后,检查mDiscoveredList,查看是否有任何项的依赖项已完整,并将这些项添加到mScheduledQueue。

12)   当mScheduledQueue中没有更多驱动需要加载时,此函数就会自动退出。

13)   显示所有加载失败的驱动的信息。

14)   在切换到BDS阶段之前报告状态码。

15)   通过EFI_BDS_ARCH_PROTOCOL找到BDS阶段的入口函数,运行如下代码来切换到BDS阶段。

  1. gBds->Entry (gBds);

 

阶段转换

1)  在PEI的结束阶段,调用PeiServicesLocatePpi来定位DXE IPL PPI,此PPI同事在FV上寻找DXE入口函数的地址。如果没有找到则报告EFI_ERROR_CODE和EFI_SW_PEI_CORE_EC_DXEIPL_NOT_FOUND,并调用CpuDeadLoop函数,启动过程停止。

2)  找到DXE入口函数后,跳转并传递HOB列表。

HOB详解

HOB是Hand-offblock的缩写。是PEI阶段向DXE传递系统信息的手段。PEI阶段构建一些HOB结构,然后将其作为参数传给DXE阶段函数,数据被打包成数据块存放在一段连续的内存中,数据块的标识为GUID,DXE阶段可以通过该GUID在HOB中找到对应数据块,根据这些数据来使用平台相关资源。DXE阶段主要使用的几类HOB数据:

1.可用内存资源信息,类型为EFI_HOB_TYPE_RESOURCE_DESCRIPTOR,用于初始化内存申请与回收服务,提供申请和回收内存的方法

2.DXE模块数据,类型为EFI_HOB_MEMORY_ALLOCATION,子类型Name=gEfiHobMemoryAllocModuleGuid,用于初始化镜像服务,提供加载、解析和执行文件的方法

3.闪存卷信息,FlashVolume,类型为EFI_HOB_TYPE_FV,对每个闪存卷建立一个PROTOCOL用于读取数据。所有的驱动数据从这里面读取,然后调度执行。


    HOB是系列的连续的内存结构体,可以认为其由三部分构成:第一部分,是PHIT头,它描述了HOB的起始地址以及总的内存使用;第二部分是各个Hob列表,DXE阶段会根据这一部分获取上关资源;第三部分是结束部分。

 

在HOB List中的第一个HOB必须是PHIT HOB(Phase Handoff Information Table),最后一个HOB必须是End of HOB List HOB。

只有PEI Phase才允许增加或改动这些HOBs,当HOB List被传送给DXE Phase之后,他们就是只读的(Read Only)。一个只读的HOB List的延伸就是Handoff 信息,比如Boot Mode,必须以别的方式来处理。比如,DXE Phase想要产生一个Recovery条件,它不能update Boot Mode,而是通过使用特殊方式的reset call来实现。

在HOB中包含的系统状态数据(System State Data)是指在PEI to DXE Handoff的时候的系统状态,而不是代表DXE当前的系统状态。

HOB在PEI到DXE传送信息的过程遵循one Producer to one Consumer的模式,即在PEI阶段,一个PEIM创建一个HOB,在DXE阶段,一个DXE Driver使用那个HOB并且把HOB相关的信息传送给其他的需要这些信息的DXE组件。

HOB list是在PEI Phase被建立的,它存在于已经present,initialized,tested的Memory中。一旦最初的HOB List被创建,物理内存就不能被remapped, interleaved, 或者被后来的程序moved。

PEI段最初HOB List中必须有以下三种HOBs

然后才能暴露这个HOB List给其他的Module(一个指针指向PHIT HOB):

1. PHIT HOB

2. 一个描述了固定存储器所在的BSP堆栈位置的Memory allocation HOB

3. 一个描述了物理内存范围的Resource descriptor HOB

在pei阶段,收集到的信息会按照如下规则进行拼装

1. 每个HOB必须以一个HOB generic header开头(EFI_HOB_GENERIC_HEADER)。

2. HOBs可以包含boot services data,在DXE Phase结束之前,PEI和DXE都可以调用。

3. HOBs可以被DXE重新安置在系统内存上,每个HOB都不能包含指向HOB List中其他数据的指针,也不能指向其他的HOB,这个Table必须可以被Copied而不需要任何内部指针的调整。

4. 所有的HOB在长度上必须是8 bytes的倍数,是alignment的要求。

5. PHIT HOB必须总是在8 byte处开始。

6. 增加的HOB总是被加到HOB List的最后,而且只能在PEI Phase(HOB Producer Phase)增加,DXE Phase(HOB Consumer Phase)不能。

7. HOBs不能被删除。每个HOB的generic header中都会描述这个HOB的长度,这样下一个HOB就很容易被找到。

增加一个新的HOB到HOB List中

PEI Phase(HOB Producer Phase)肯定包含一个指向PHIT HOB(这是HOB List的开始)的指针,然后遵循以下的步骤:

1. 确定NewHobSize,即确定要创建的HOB的大小(以Byte为单位)。

2. 确定是否有足够的空闲内存分配给新的HOB(NewHobSize <= (PHIT->EfiFreeMemoryTop - PHIT->EfiFreeMemoryBottom))。

3. 在(PHIT->EfiFreeMemoryBottom)处构建HOB。

4. 设置PHIT->EfiFreeMemoryBottom = PHIT->EfiFreeMemoryBottom + NewHobSize 。

 

调度器

由于不同设备的驱动互相之间存在一定的依赖关系,而调度器不能保证总是先找到依赖链中较为靠前的驱动,因此在DXE调度器中,采用了两个队列。其中一个队列存放当前已经满足依赖项,可以加载的驱动的指针,另一个则存放当前已经找到但还未满足加载条件的所有驱动的指针,调度器每循环一次都会完成第一个队列中一个驱动的加载,同时将第二个队列中的项目检查一遍,若已满足依赖项则移到第一个队列中。流程如下图所示:

 

 

两种驱动类型

根据是否符合UEFI Driver Model的规范来分,一种是不符合的普通Driver,一种是符合该模型规范的标准驱动。在DXE阶段,这两种类型的驱动,其加载流程是不同的。前者在DXE阶段被找到时就完成运行,而后者在DXE阶段先完成注册,然后在BDS阶段才真正运行初始化,通过调用系统服务来完成。驱动被调用的基本函数是LoadImage()和StartImage(),它们是Boot Service,所以可以在DXE和BDS阶段的大部分地方调用。

在DxeMain.c文件中的DxeMain()函数会调用CoreDispatcher(),也就是DXE阶段的核心调度器,就是用来执行各个驱动的,除非自己修改代码,否则DXE驱动都会在这个位置执行。

 

 

两种类型驱动的代码结构上类似,主要的区别是驱动的入口做了什么。

下面是一个驱动的inf文件的定义部分:

 

[Defines]

  INF_VERSION                    = 0x00010005

  BASE_NAME                      = DxeDriverInBds

  FILE_GUID                      = 04687443-0174-498F-A2F9-08F3A5363F84

  MODULE_TYPE                    = UEFI_DRIVER

  VERSION_STRING                 = 1.0

  ENTRY_POINT                    = DxeDriverEntry

 

最后一行是C代码的入口,对于普通的驱动,这个入口里面就是初始化设备的函数。

a. 在DXE Phase最早执行的Driver

b. 包含Dependency Expression Syntax(DEPEX) 来描述Dispatch的顺序。

c. 典型的包含:

              Basic Services

              Processor Initialization Code

              Chipset Initialization Code

              Platform Initialization Code

d. 产生Architectural Protocols

 

而对于符合UEFI Driver Model的驱动来说,它只是简单的安装了一个Protocol。

        a. 初始化的过程中不会涉及到硬件

        c. 典型的提供对Console Devices 和 Boot Devices的访问

        d. Abstract Bus Controller

        e. 只有Boot OS 所需要的Driver才被初始化

        f. DXE Dispather完成的时候才被呼叫

        g. 像个Driver一样被执行

        h. 需要建立控制台(Keyboard,Video)和处理EFI Boot Option(Boots OS)的时候要连接EFI Drivers。

接下来以SnpDxe模块为例看看驱动入口的形式:

 

EFI_STATUS

EFIAPI

InitializeSnpNiiDriver (

  IN EFI_HANDLE       ImageHandle,

  IN EFI_SYSTEM_TABLE *SystemTable

  )

{

  return EfiLibInstallDriverBindingComponentName2 (

           ImageHandle,

           SystemTable,

           &gSimpleNetworkDriverBinding,

           ImageHandle,

           &gSimpleNetworkComponentName,

           &gSimpleNetworkComponentName2

           );

}

 

这里的重点在于gSimpleNetworkDriverBinding这个Protocol,它的形式如下:

 

EFI_DRIVER_BINDING_PROTOCOL gSimpleNetworkDriverBinding = {

  SimpleNetworkDriverSupported,

  SimpleNetworkDriverStart,

  SimpleNetworkDriverStop,

  0xa,

  NULL,

  NULL

};

 

所有的符合UEFI Driver Model的驱动都会安装一个如上结构的Protocol,在《UEFI Spec》里面有对该类型Protocol的详细介绍。

 

简单来说,就是DXE阶段安装了一大堆这种Protocol,然后gBS->ConnectController的时候,首先会执行xxxSupported()函数,如果返回的是EFI_SUCCESS,则会继续执行xxxStart()函数,而这个函数中就包含设备初始化所需要的代码。大概流程如下:

1. 当扫描的这个设备的时候(设备用Controller表示),先判断它是否安装了DevicePathProtocol,没有就表示这个设备还没有准备好(或者说不是设备),后面的xxxStart()不用执行;

2. 然后判断NetworkInterfaceIdentifierProtocol是否安装,这个是网卡驱动一定会装的Protocol,Snp驱动底层的操作需要依赖于它,所以一定要安装,如果没有就不会执行后面的操作;

3. 判断NetworkInterfaceIdentifierProtocol是否满足要求,如果不满足则不会执行xxxStart()函数。

如果以上条件都满足,就可以认为该设备是一个网卡,然后这个驱动就会被执行,而之前获取到的DevicePathProtocol和NetworkInterfaceIdentifierProtocol就会成为操作正确设备的基础。