EDK2的模块(Module)和包(Package)
在 EDK2 根目录下,有很多以*Pkg
命名的文件夹,每一个这样的文件夹称为一个 Package。准确地说,“包”是一组模块及平台描述文件(.dsc 文件)包声明文件(.dec文件)组成的集合。
模块(可执行文件即.efi
文件)像插件一样可以动态地加载到 UEFI内核中。对应到源文件,EDK2 中的每个工程模块由元数据文件(.inf
文件)和源文件(有些情况下也可以包含.efi
文件)组成。
在 EDK2 环境下,除了要编写源文件外,还要为工程编写元数据文件(.inf
)。.inf
文件与 Linux 下的 Makefle
文件功能相似,用于自动编译源代码。
模块类型 | 说明 |
---|---|
标准应用程序工程模块 | 在 DXE 阶段运行的应用程序(Shel环境下也可以运行) |
ShellAppMain 应用程序工程模块 | Shell 环境下运行的应用程序 |
main 应用程序工程模块 | Shell 环境下运行的应用程序,并且应用程序链接了 StdLib 库 |
UEFI 驱动模块 | 符合 UEFI 驱动模型的驱动,仅在 BS 期间有效 |
库模块 | 作为静态库被其他模块调用 |
DXE 驱动模块 | DXE 环境下运行的驱动,此类驱动不遵循 UEFI 驱动模型 |
DXE 运行时驱动模块 | 进入运行期仍然有效的驱动 |
DXE SAL 驱动模块 | 仅对安腾CPU有效的一种驱动 |
DXE SMM 驱动模块 | 系统管理模式驱动,模块被加载到系统管理内存区。系统进入运行期该驱动仍然有效 |
PEIM 模块 | PEI 阶段的模块 |
SEC 模块 | 固件的 SEC 阶段 |
PEI_CORE 模块 | 固件的 PEI 阶段 |
DXE_CORE 模块 | 固件的 DXE 阶段 |
标准应用程序工程模块
标准应用程序工程模块是其他应用程序工程模块的基础,也是 UEFI 中常见的一种应用程序工程模块。每个工程模块由两部分组成:工程文件 和 源文件,标准应用程序工程模块也不例外。
源文件包括 C/C++ 文件、.asm
汇编文件,也可以包括 .uni
(字符串资源文件)和 .vf
(窗体资源文件)等资源文件。
入口函数
一般来说,标准应用程序至少要包含以下两个部分。
#include <Uefi.h>
EFI_STATUS
EFIAPI
UefiMain(IN EFI_HANDLE Imagehandle, IN EFI_SYSTEM_TABLE *SystemTable) {
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"MY first HelloWorld\n");
return EFI_SUCCESS;
}
1)头文件:所有的 UEFI 程序都要包含头文件 Uefi.h
。Uefi.h
定义了 UEFI 基本数据类型及核心数据结构。
2)入口函数:UEFI 标准应用程序的入口函数通常是 UefiMain
。之所以说通常是 UefiMain
而不是说必须是 UefiMain
,是因为入口函数可有开发者指定。UefiMain
只是一个约定俗成的函数名。入口函数由工程文件 UefiMain.inf
指定。虽然入口函数的函数名可以变化,但其函数签名(即返回值类型和参数列表类型)不能变化。
①入口函数的返回值类型是 EFI_STATUS
在 UEFI 程序中基本所有的返回值类型都是 EFISTATUS
。它本质上是无符号长整数;
最高位为 1
时其值为错误代码,为 0
时表示非错误值。通过宏 EFI_ERROR(Status)
可以判断返回值 Status
是否为错误码。若 Status
为错误码,EFIERROR(Status)
返回真否则返回假。
EFISUCCESS
为预定义常量,其值为0,表示没有错误的状态值或返回值
②入口函数的参数 lmageHandle 和 SystemTable
.efi
文件(UEFI 应用程序或 UEFI 驱动程序)加载到内存后生成的对象称为 Image(映像)。ImageHandle
是 Image 对象的句柄,作为模块入口函数参数,它表示模块自身加载到内存后生成的 Image 对象。
SystemTable 是程序同 UEFI 内核交互的桥梁,通过它可以获得 UEFI 提供的各种服务,如启动(BT)服务和运行时(RT)服务。SystemTable 是 UEFI 内核中的一个全局结构体。
向标准输出设备打印字符串是通过 SystemTable
的 ConOut
提供的服务完成的。
ConOut
是 EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
的一个实例。而 EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
的主要功能是控制字符输出设备。
向输出设备打印字符串是通过 ConOut
提供的OutputString
服务完成的。该服务(函数)的第一个参数是 This
指针,指向 EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
实例(此处为ConOut
)本身;第二个参数是 Unicode
字符串。
通过 SystemTable→ConOut→OutputString 服务将字符串 L“HelloWorld”
打印到 SystemTable→ConOut 所控制的字符输出设备。
工程文件
要想编译 Main.c
,还需要编写.inf
(Module Information File)文件,用于指导 EDK2 编译工具自动编译模块。
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = hello
FILE_GUID = f35a7352-2cc1-44c0-9ba6-4c3b5f4dbe42
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 1.0
ENTRY_POINT = UefiMain
[sources]
../myhello.c
[Packages]
MdePkg/MdePkg.dec
[LibraryClasses]
UefiApplicationEntryPoint
UefiLib
工程文件分为很多个块,每个块以“[块名]”开头,“[块名]”必须单独占一行。
工程文件必需块
必需块 | 块描述 |
---|---|
[Defines] | 定义本模块的属性变量及其他变量,这些变量可在工程文件其他块中引用 |
[Sources] | 列出本模块的所有源文件以及资源文件 |
[Packages] | 列出本模块引用到的所有包的声明文件。可能引用到的资源包括头文件、GUID、Protocol 等,这些资源都声明在包的包声明文件 .dec 中 |
[LibraryClasses] | 列出本模块要链接的库模块 |
[Defines] 块:用于定义模块的属性和其他变量,块内定义的变量可被其他块引用。
1)属性定义语法:属性名=属性值
2)属性块内必须定义的属性包括:
INF_VERSION
:INF标准的版本号。EDK2 的 build
会检查 INF_VERSION
的值并根据这个值解释.inf
文件。
BASE_NAME
:模块名字字符串,不能包含空格。它通常也是输出文件的名字。
FILE_GUID
:每个工程文件必须有一个 8-4-4-4-12
格式的 GUID
,用于生成固件。
VERSION_STRING
:模块的版本号字符串。
MODULE_TYPE
:定义模块的模块类型.
ENTRY_POINT
:定义模块的入口函数。
[Sources] 块:用于列出模块的所有源文件和资源文件。
1)语法:块内每一行表示一个文件,文件使用相对路径,根路径是工程文件所在的目录。
2)体系结构相关块:
[Sources.$ (Arch)]
.$(Arch)
是可选项,可以是IA32
、X64
、IPF
、EBC
、ARM
中的一个,表示本块适用的体系结构。[Sources]
块适用于任何体系结构。例如,编译 32 位模块时(即在 build 命令选项中使用了-a IA32
选项),工程将包含 [Sources]
和 [Sources.IA32]
中的源文件;编译 64 位模块时,工程将包含 [Sources] 和 [Sources.X64] 中的源文件。
3)编译工具链相关的源文件:列出本模块引用到的所有包的包声明文件(.dec
文件)。
[Sources]
Timerwin.c | MSFT
TimerLinux.c | GCC
[Packages] 块:列出本模块引用到的所有包的包声明文件(.dec
文件)。
1)语法:[Packages]
块内每一行列出一个文件,文件使用相对路径,相对路径的根路径为 EDK2 的根目录。若 [Sources]
块内列出了源文件,则在 [Packages]
块必须列出 MdePkg/MdePkgdec
,并将其放在本块的首行。
[LibraryClasses]:块列出本模块要链接的库模块。
1)语法:块内每一行声明一个要链接的库(库定义在包的 .dsc
文件中,定义方法将在下文讲述)。语法如下:
[LibraryClasses] 库名称
2)常用库:应用程序工程模块必须链接 UefiApplicationEntryPoint 库;驱动模块必须链接 UefiDriverEntryPoint 库。
[LibraryClasses] UefiApplicationEntryPoint
[Protocols] 块:列出模块中使用的 Protocol,严格来说,列出的是 Protocol 对应的 GUID。如果模块未使用任何 Protocol,则此块为空。
1)语法:块内每一行声明一个在本模块中引用的 Protocol。语法如下
[LibraryClasses] Protocol 的 GUID
[BuildOptions] 块:指定本模块的编译和连接选项。
1)语言:
[BuildOptions]
[编译器家族]:[$(Target)][TOOL_CHAIN_TAG][$(Arch)][CC|DLINK]FLAGS[=|==] 选项
编译器家族
可以是 MSFT(Visual Studio编译器家族)、INTEL(Intel 编译器家族)、(GCC 编译器家族)和 RVCT (ARM 编译器家族)中的一个。
Target
是 DEBUG、RELEASE 和*
中的一个,*
为通配符,表示对 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 表示连接链。
=
表示选项附加到默认选项后面。 ==
表示仅使用所定义的选项,弃用默认选项。
=
或 ==
后面是编译器选项或链接选项。
工程文件非必需文件
非必需块 | 块描述 |
---|---|
[Protocols] | 列出本模块用到的 Protocol |
[Guids] | 列出本模块用到的 GUID |
[BuildOptions] | 指定编译和链接选项 |
[Pcd] | Pcd 全称为平台配置数据库,用于列出本模块用到的 Pcd 变量,这些 Pcd 变量可被整个 UEFI 访问 |
[PcdEx] | 用于列出本模块用到的 Pcd 变量,这些 Pcd 变量可被整个 UEFI 访问 |
[FixedOcd] | 用于列出本模块用到的 Pcd 编译期常量 |
[FeaturePcd] | 用于列出本模块用到的 Pcd 常量 |
[PatchPcd] | 列出的 Pcd 变量仅本模块可用 |
编译和执行
将 .inf
添加到 .dsc
的 [Components]
部分
然后,Source edksetup.sh && build
选择:fs0:
后执行,运行 .efi
文件.
标准应用程序的加载过程
应用程序被编译成 .efi
文件,整个过程分为以下三步:
1)UefiMain.c
首先被编译成目标文件 UefiMain.obj
;
2)连接器将目标文件 UefiMain.obj
和其他库连接成 UefiMain.dll
;
3)GenFw
工具将 UefiMain.dll
转换成 UefiMain.efi
。
1. 将 UefiMain.efi 文件加载到内存
当在 Shell 中执行 UefiMain.efi
时Shell
首先用 gBS->LoadImage()
将 UefiMain.efi
文件加载到内存生成 Image
对象,然后调用 gBS->StartImage(lmage)
启动这个 Image
对象。
a. 将 UefiMain.efi
文件加载到内存,生成 Image
对象,NewHandle
是这个对象的句柄;
b. 取得命令行参数,并将命令行参数交给 UefiMain.efi
的 Image 对象,即 NewHandle
;
c. 启动所加载的 Image
。
加载应用程序中最重要的一步,是gBS->StartImage(NewHandleNULL,NULL)
。
Startlmage
的主要作用是找出可执行程序映像(Image)的入口函数并执行找到的入口函数。gBS->StartImage
是个函数指针,它实际指向 CoreStartlmage
函数。
2. 进入印象的入口函数
CoreStartlmage
函数的主要作用是调用印象的入口函数。
gBS->Startlmage
的核心是 Image->EntryPoint(…)
,它就是程序映像的入口函数,对应用程序来说,就是_ModuleEntryPoint
函数,进入后,控制权才转交给应用程序。
_ModuleEntryPoint
主要处理以下三个事情
1)初始化:在初始化函数 ProcessLibraryConstructorList
中会调用一系列的构造函数。
2)调用本模块的入口函数:在ProcessModuleEntryPointList
中会调用应用程序工程模块真正的入口函数(即我们在.inf文件中定义的人口函数 UefiMain)。
3)析构:在析构函数 ProcessLibraryDestructorList
中会调用一系列析构函数。
这三个 Process*函数 是在命令行执行 build
命令时,build
命令会解析模块的工程文件(即.inf
文件),然后生成 AutoGen.h
和AutoGen.c
,这三个函数是 AutoGen.c
中的一部分。
一般而言,在 .inf
文件的 [LibraryClasses]
段声明了某个库后,如果这个库有构造函数,AutoGen
便会在 ProcessLibraryConstructorList
中加入这个库的构造函数。另外,ProcessLibraryConstructorList
还会加入启动服务和运行时服务的构造函数。
gBS 指向启动服务表,gST 指向系统表(SystemTable),gImageHandle 指向正在执行的驱动或应用程序,gRT 指向运行时服务表。
3.进入模块入口函数
在 ProcessModuleEntryPointList
中,调用了应用程序工程模块的真正入口函数。
整个过程:StartImage -> ModuleEntryPoint -> ProcessModuleEntryPointList -> UefiMain。
其他类型工程模块
1. Shell 应用程序工程模块
标准应用程序处理命令行参数很不方便,而能在 Shell 中执行的命令(命令也是应用程序)通常都会带有命令行参数,为了方便开发者开发能在Shell 环境下执行的应用程序,EDK2 提供了一种特殊的应用程序工程模块,这种模块以 INTN ShellAppMain(IN UINTN Argc,IN CHAR16**Argv)
作为入口函数。称这种模块为Shell应用程序工程模块。
因为在入口函数的参数中没有了 SystemTable
,所以要通过全局变量 gST
使用系统表。入口函数 ShellAppMain
的第一个参数 Argc
是命令行参数个数,第二个参数 Argv
是命令行参数列表,Argv
列表中的每个参数都是Unicode
字符串(CHAR16*类型的字符串)。在UEFI中使用的字符串通常都是Unicode
字符串。 【这里和 C 中几乎一模一样】
其工程文件部分,将必要的一些改为 SHELL 版本就可以。
2. 使用 main 函数的应用程序工程模块
EDK2 也提供了使用 main 函数的应用程序工程模块,通常在此类应用程序中都会使用 C 标准库(StdLib
) 中的函数。
在使用 main 函数的应用程序工程模块中使用了StdLib
,而 StdLib
提供了 ShellAppMain
函数。开发者要实现int main(int Argc,char**ArgV)
作为程序的人口函数以供 ShellAppMain
调用。而真正的模块入口函数是 ShellCEntryLib
,调用过程为 ShellCEntryLib -> ShellAppMain -> main
。
此外,如果用户的程序中用到了printf(.)
等标准C的库函数,那么一定要使用此种类型的应用程序工程模块。ShellCEntryLib
函数中会调用StdLib
的ShellAppMain(..)
,这个 ShellAppMain
函数会对 StdLib
进行初始化。StdLib
的初始化完成后才可以调用 StdLib
的函数。
通常,使用main函数的应用程序工程模块在AppPkg环境下才能成功编译。首先将main.inf添加到 AppPkg\AppPkg.dsc
文件的[Components]
。
3. 库模块
在库模块的工程文件中,需要设置MODULET_YPE
为BASE
;设置LIBRARY_CLASS
为library
的名字;同时,不要设置 ENTRY_POINT
。
[Packages]
块列出库引用到的包,[LibraryClasses]
列出包所依赖的其他库。
有些库仅能被某些特定的模块调用,编写这种库时需在工程文件中声明库的适用范围声明方法是在[Defines]
块的 LIBRARY_CLASS
变量中定义,格式如下:
LIBRARY_CLASS = 库名字 | 适用模块类型1 适用模块类型2
编写了库之后,要使库能被其他模块调用,还要在包的.dsc
文件中声明该库。
例如要使得 AppPkg
中的模块能调用 zlib
库,需将 zlib|zlib-1.2.6/zlib.inf
(假设 zlib-1.2.6
在EKD2
的根目录下)放到AppPkg\AppPkg.dsc
文件[LibraryClasses]
中,调用 zlib
时,在需要链接 zlib 的工程模块的工程文件 [LibraryClasses]
中添加 zlib
即可。
如果库使用之前需要进行初始化,在库的工程文件需指定 CONSTRUCTOR
和 DESTRUCTOR
,CONSTRUCTOR
函数会加入到ProcessLibraryConstructorList
中,这个 CONSTRUCTOR
函数会在 ENTRY_POINT
之前执行;DESTRUCTOR
函数会加入到ProcessLibraryDestructorList
中,这个 DESTRUCTOR
就会在ENTRY_POINT
后执行。
4. UEFI 驱动模块
在 UEFI 中,驱动分为两类:一类是符合 UEFI 驱动模型的驱动,模块类型为 UEFI_DRIVER
称为 UEFI驱动;另一类是不遵循 UEFI 驱动模型的驱动,模块类型包括 DXE_DRIVER
,DXE_SAL_DRIVER
、DXE_SMM_DRIVER
、DXE_RUNTIME_DRIVER
,称为 DXE 驱动。
驱动与应用程序的模块入口函数(ENTRYPOINT)类型一样;
驱动与应用程序的最大区别是驱动会常驻内存,而应用程序执行完毕后就会从内存中清除。
包及 .dsc、.dec、.fdf 文件
build
命令用于编译包,它需要一个.dsc
文件、一个.dec
文件以及一个或多个.inf
文件;
GenFW
命令用于制作固件或Option Rom Image,它需要一个.dec
文件和一个 .fdf
文件。
.inf
用于编译一个模块,而 .dsc
文件用于编译一个 Package
,它包含了[Defines]、[LibraryClasses]、[Components]
几个必需部分以及 [PCD]、[BuildOptions]
等几个可选部分。
[Defines]
中通过DEFINE
和EDK_GLOBAL
定义的宏可以在.dsc
文件和.fdf
文件中通过 $(宏变量名) 使用。- 在
[ibraryClasses]
块中定义了库的名字以及库.inf
文件的路径。这些库可以被[Components]
块内的模块引用。 - 在
[Components]
块内定义的模块都会被 build 工具编译并生成.efi文件; .dsc
文件的[BuildOptions]
与.inf
文件的[BuildOptions]
格式大致相同,区别在于.dsc
文件的[BuildOptions]
对.dsc
文件内的所有模块有效;
.dec
文件定义了公开的数据和接口,供其他模块使用。它包含了必需区块 [Defines]
以及可选区块 [Includes]
、[LibraryClasses]
、[Guids]
、[Protocols]
、[Ppis]
和[PCD]
几个部分。
[Defines]
区块用于提供 package 的名称、GUID、版本号等信息;[Imncludes]
中列出了本Package 提供的头文件所在的目录;- Package 可以通过
.dec
文件对外提供库,每个库都必须有一个头文件,放在Include\Library
目录下。本区块用于明确库和头文件的对应关系; - 在
Package\nclude\Guid
目录中有很多文件,每个文件内定义了一个或几个GUID
; - 与Guids 类似,在
Package\nclude\Protocols
目录下有很多头文件,每个头文件定义了一个或多个Protocol
,这些Protocol
的GUID
值就定义在.dec 文件的[Protocols]
区块。
内容基本全部来自于 《UEFI原理与编程》、、