UEFI 工程模塊文件-標準應用程序工程模塊
前言
在EDK2環境下編程之前,先介紹EDK2的兩個概念模塊(Module)和包(Package).
“包”是一組模塊及平臺描述文件(.dsc文件)、包聲明文件(.dec文件)則、組成的集合,多在以*pkg命名的文件夾中,一般也稱這樣的文件夾爲一個包。
模塊是UEFI系統的一個特色。模塊(可執行文件,即.efi文件)像插件一樣可以動態地加載到UEFI內核中。對應到源文件,EDK2中的每個工程模塊由元數據文件(.inf)和源文件(有些情況也可以包含.efi文件)組成。
主要介紹3種應用程序模塊、UEFI驅動模塊和庫模塊。
一、標準應用程序工程模塊
標準引用程序工程模塊是其它應用程序模塊的基礎,也是UEFI中常見的一種應用程序模塊。每個工程模塊由兩部分組成:工程文件和源文件。源文件包括C/C++文件、.asm彙編文件,也可以包括.uni(字符串資源文件)和.vrf(窗體資源文件)等資源文件。
1.源文件
示例程序:
//hello.c
#include<Uefi.h>
EFI_STATUS UefiMain(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable)
{
SystemTable->ConOut->OutputString(SystemTable->ConOut,L"Hello man,\n welcome to UEFI world\n");
return EFI_SUCCESS;
}
- 頭文件:所有的標準應用程序工程模塊的源文件的頭文件都要包含Uefi.h。Uefi 定義了UEFI基本數據類型和核心數據結構。
- 入口函數:UEFI標準應用程序的入口函數通常是
UefiMain
,它是約定成俗的函數,它是可以在.inf
文件中指定。它的函數簽名(返回值類型和參數列表類型)是不能變化的。
- 入口函數的返回值類型是
EFI_STATUS
。
- 在UEFI程序中基本所有的返回值類型都是EFI_STATUS。它的本質是無符號長整數
- 最高位爲1時其值爲錯誤代碼,爲0時表示非錯誤值。通過宏EFI_ERROR(Status)可以判斷返回值Status時候爲錯誤代碼。若Status爲錯誤代碼EFI_ERROR(Status)返回值爲真,否則爲假。
- EFI_SUCCESS爲預定義常量,其值爲0,表示沒有錯誤的狀態值和返回值。
- 入口函數的返回值類型是
入口函數參數
ImageHandle
和SystenTable
- .efi文件(UEFI應用程序或UEFI驅動程序)加載到內存後生成的對象成爲
Image
(映像)。ImageHandle
是Image
的句柄,作爲模塊入口函數的參數,它表示模塊自身加載到內存後生成的Image
對象 。 SystemTable
是程序同UEFI內核交互的橋樑,通過它可以獲得UEFI提供的各種服務(BT/RT),SystemTable
是UEFI內核的一個全局結構體。
向標準輸出設備打印字符串是通過
SystemTable
的ConOut
提供的OutputString
服務完成的。ConOut
是EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
的一個實例,而EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
主要功能是控制字符輸出設備。OutputString
服務的第一個參數是指向EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
的一個實例即:ConOut
。第二個參數是Unicode字符串。簡單講就是SystemTable->ConOut->OutputString
服務將字符串L”Hello World”打印到SystemTable->ConOut
所控制的字符串輸出設備。- .efi文件(UEFI應用程序或UEFI驅動程序)加載到內存後生成的對象成爲
2.工程文件
工程模塊分爲很多塊,每個塊以“[快名]”開頭,它必須單獨佔一行。有些塊是所有工程文件都必需的塊,這些塊包括[Defines]、[Sources]、[Packages]和[LibraryClass]。
詳細的工程模塊如下表:
[Defines]塊
[Defines]塊用於定義模塊的屬性和其它變量,塊內定義的變量可以被其它塊引用。- 屬性定義的語法
屬性名 = 屬性值 - 塊內必須屬性
- INF_VERSION:INF標準版本號。EDK2的build會檢查INF_VERSION 的值並根據這個值解釋
.inf
文件。設置爲0x00010006或0x00010005。 - BASE_NAME:模塊名字符串,不能包含空格。它通常也是輸出文件的名字。
- FILE_GUID:每個工程文件必須有一個
8-4-4-4-12
格式的GUID用於生成固件。(每一位數十六進制 0-F) - VERSION_STRING:模塊的版本號,一般設置爲
1.0
,根據自己寫的模塊版本設定即可。 - MODULE_TYPE:定義模塊的模塊類型,對於標準應用模塊,設爲UEFI_APPLICATION.
- ENTRY_POINT:定義模塊的入口函數,根據在源文件中的入口函數填寫。一般是UefiMain。
- INF_VERSION:INF標準版本號。EDK2的build會檢查INF_VERSION 的值並根據這個值解釋
- 屬性定義的語法
[Defines]
INF_VERSION = 0x00010006
BASE_NAME = HelloWorld
FILE_GUID = 4ea97c46-7491-4dfd-b442-747010f3ce5f
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 1.0
ENTRY_POINT = UefiMain
- [Sources]塊
用於列出模塊的所有源文件和資源文件。
- 語法
塊內每一行表示一個文件,文件使用相對工程文件的路徑。 - 體系結構相關塊
可以在使用Sources.$(Arch)
,其中$(Arch)
是表示本塊的體系結構,可以是IA32
,X64
,IPF
,EBC
,ARM
中一個。這個的作用是不同的體系結構可能包含的源文件或資源文件不同,如果都寫進[Sources]
可能有問題,但是可以列出對應的[Sources.$(Arch)]
,然後根據編譯時標識設置,[Sources]
都會被編譯,[Sources.$(Arch)]
中和標識相符的纔會被編譯。 - 編譯工具鏈相關的源文件
有是文件後跟工具鏈的符號表示只有在該工具鏈編譯器編譯時有效。
- MSFT : Visual Stdio
- INTEL : ICC編譯器
- RVCT : ARM編譯器
- 語法
//體系結構相關塊示例
[Sources]
Common.c
[Sources.IA32]
Cpu32
[Sources.X64]
Cpu64
//編譯工具鏈相關的源文件示例
[Sources]
TimerWin.c | MSFT
TimerLinux.c | GCC
- [Packages]塊
[Packages]列出本模塊引用到的所有包的聲明(.dec)文件。
- 語法
[Packages]塊內每一行列出一個文件,文件使用相對於EDK2根目錄的路徑。若[Sources]列出了源文件,則[Packages]塊必須列出MdePkg/MdePkg.dec,並將其放在本塊首行。
- 語法
- [LibraryClasses]
[LibraryClasses]塊列出本模塊要連接的庫模塊。
- 語法
塊內每一行聲明一個要連接的庫(庫的定義在.dsc文件中) - 常用庫
應用程序工程模塊必須連接UefiApplicationEntryPoint庫,驅動模塊必須連接UefiDriverEntryPoint庫。
- 語法
非必須塊(如果有用到,則需要寫出)
- [Protocols]塊
[Protocol]列出的模塊中使用的Protocol,實際上是Protocol對應的GUID,如果未使用則爲空。 - [BuildOptions]塊
- 語法
[BuildOptions]
[編譯器家族]
:[$
(Target)][TOOL_CHAIN_TAG][$(Arch)]_[CC|DLINK]_FLAGS[=|==]選項
- 編譯器家族:MSFT、INTEL、GCC、RVCT。
- Target:DEBUG、RELEASE、*(對前兩個都有效)。
- TOOL_CHAIN_TAG編譯器名字,定義在Conf\tools_def.txt文件中,與定義編譯器名字:VS2003,VS2005,VS2008,VS2010,GCC44,GCC45,GCC46,CYGGCC,ICC等 ,*表示對指定家族的編譯器都有效。
- Arch是體系結構,與前述相同,可以是
IA32
,X64
,IPF
,EBC
,ARM
中一個,* 對所有體系結構有效。 - CC表示編譯選項,DLINK表示連接選項
- =表示選項附加到默認選項後面,==表示僅使用所定義的選項,棄用默認選項
- =,==後面接選項
- 語法
注:
這是個很有用的選項,我們寫正常C程序時一些無關緊要的警告在EDK2編譯模塊文件時會將它是做錯誤。所以可以使用下面的[BuildOptions]
可以避免將這些警告堪稱錯誤。
[BuildOptions]
MSFT:*_*_*_CC_FLAGS = /w
下面是HelloWorld的.inf
文件
// HelloWorld.inf
[Defines]
INF_VERSION = 0x00010006
BASE_NAME = HelloWorld
FILE_GUID = 4ea97c46-7491-4dfd-b442-747010f3ce5f
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 1.0
ENTRY_POINT = UefiMain
[Sources]
HelloWorld.c
[Packages]
MdePkg/MdePkg.dec
[LibraryClasses]
UefiApplicationEntryPoint
UefiLib
[BuildOptions]
MSFT:*_*_*_CC_FLAGS = /w
三、編譯運行
1. 添加工程文件
將工程文件即HelloWorld.inf 添加到NT32Pkg.dsc的[Components]部分。
2. 加載EDK2環境
打開MS-DOS(cmd),進入到EDK2的根目錄,用edksetup.bat
加載EDK2環境。
3. 輸入
build -p Nt32Pkg\Nt32Pkg.dsc -m [helloworld.inf相對於EDK2根目錄的相對路徑名] -a IA32
(64位用X64)
4. build 信息
build 成功後畫面如下:
輸出內容裏也會指明文件輸出的路徑。
5. 運行
在MS-DOS輸入 build run
進入UEFI模擬環境,運行前面目錄下的HelloWorld.efi文件。
注:可以輸入fs0:
快速進入/EDK2/Build/NT32IA32/DEBUG_VS2008/IA32/
6. 運行結果
四、標準應用程序加載過程
編譯過程:
1. HelloWorld.c 首先被編譯成目標文件 HelloWorld.obj
2. 連接器將目標文件HelloWorld.c 和其它庫連接成HelloWorld.dll。
3. GenFw 工具將HelloWorld.dll 轉化成 HelloWorld.efi。
上述過程由 build
命令自動完成,連接器在生成HelloWorld.dll時使用了/dll/entry:_ModuleEntryPoint。.efi是遵循了PE32格式的二進制文件,_ModuleEntryPoint便是這個二進制文件的入口函數。下面探討應用程序加載過程,主要看_ModuleEntryPoint和源文件中入口函數UefiMain的關係。
1. 將HelloWorld.efi 文件加載到內存
當shell中執行HelloWorld.efi時,shell首先用gBS->LoadImage()將HelloWorld.efi文件加載到內存生成Image對象,然後調用gBS->StartImag(Image)啓動這個Image對象。gBS->StartImage()是一個函數指針,它實際指向的是CoreStartImage()
2. 進入映像入口函數
CoreStartImage()的主要作用是調用映像入口函數,在gBS->StartImage 的核心是Image->EntryPoint(···),它就是程序映像的入口函數,對應程序來說就是_ModuleEntryPoint 函數。進入 _ModuleEntryPoint 後,控制權才轉交給應用程序(HelloWorld.efi)。
_ModuleEntryPoint主要處理三件事:
1. 初始化:初始化函數ProcessLibraryConstructorList中調用一系列構造函數
2. 調用本模塊的入口函數 : ProcessModuleEntryPointList 中調用的是工程模塊定義的入口函數
3. 析構:ProcessLibraryDestructorList 中調用一系列析構函數。
這三個對應的函數AutoGen.h,AutoGen.c中。
3. 進入模塊入口函數
在ProcessModuleEntryPointList函數中調用了工程模塊的真正入口函數UefiMain。
五、總結
標準應用程序模塊是其它應用程序模塊的基礎,需要對它熟悉使用掌握,後續會接着介紹其它類型的工程模塊。
另外,此篇文章後的demo是在已經安裝了EDK2環境基礎上編譯運行的。如果還沒有安裝,可以參考:UEFI原理與編程(一):環境搭建。
參考資料
<1>《UEFI原理與編程》戴正華 著
<2> UEFI Spec2_6
<3> 百度百科