UEFI啟動過程與協議加載順序


在關鍵的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就會成為操作正確設備的基礎。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM