作者:Maxwell Li
日期:2017/12/05
未经作者允许,禁止转载本文任何内容。如需转载请留言。
[TOC]
“包”是一组模块及平台描述文件(.dsc)、包声明文件(.dec)组成的集合。
模块(.efi)像插件一样可以动态地加载到 UEFI 内核中。
在 EDK2 环境下,除了要编写源文件外,还要为工程编写元数据文件(.inf)。
3.1 标准应用程序工程模块
标准引用程序工程模块是其它应用程序模块的基础,也是UEFI中常见的一种应用程序模块。每个工程模块由两部分组成:工程文件和源文件。
3.1.1 源文件
示例程序:
#include<Uefi.h>
EFI_STATUS UefiMain(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable)
{
SystemTable->ConOut->OutputString(SystemTable->ConOut,L"HelloWorld\n");
return EFI_SUCESS;
}
标准应用程序至少包含以下两个部分:
- 头文件:所有的 UEFI 程序都要包含头文件 Uefi.h。该文件定义了 UEFI 基本数据类型及核心数据结构。
- 入口函数:入口函数由工程文件 UefiMain.inf 指定,通常是 UefiMain,其函数签名(返回值类型和参数列表类型)不能变化。
入口函数返回值类型是 EFI_STATUS。
- UEFI 程序中基本上所有返回值类型都是 EFI_STATUS,其本质是无符号长整数。
- 最高位为1时其值为错误代码,最高位为0时表示非错误值。若返回值 Status 为错误码,宏 EFI_ERROR(Status) 返回真,否则返回假。
- EFI_SUCESSS 为预定义常量,值为0,表示没有错误的状态值和返回值。
入口函数的参数 ImageHandl 和 SystemTable。
- .efi 文件加载到内存后生成的对象称为 Image。ImageHandle 是 Image 的句柄,作为模块入口函数的参数,它表示模块自身加载到内存后生成的 Image 对象。
- SystemTable 是程序和 UEFI 内核交互的桥梁,通过他可以获得 UEFI 提供的各种服务。SystemTable 是 UEFI 内核的一个全局结构体。
向标准输出设备打印字符串是通过 SystemTable 的 ConOut 提供的 OutputString 服务完成的。ConOut 是 EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL 的一个实例,而 EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL 主要功能是控制字符输出设备。OutputString 服务的第一个参数是 This 指针,指向 EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL 实例(此处为 ConOut 本身)。第二个参数是 Unicode 字符串。这条打印语句的意义就是通过 SystemTable->ConOut->OutputString 服务将字符串 L“Hello World” 打印到 SystemTable->ConOut 所控制的字符串输出设备。
3.1.2 工程文件
工程文件分为很多块,详细的工程模块如下所示:
[Defines]
[Defines] 块用于定义模块的属性和其他变量,块内定义的变量可以被其他块应用。
- 语法:属性名 = 属性值
- 块内必须属性:
- INF_VERSION:INF 标准版本号。EDK2 的 build 会检查 INF_VERSION 的值并根据这个值解释 .inf 文件。
- BASE_NAME:模块名字字符串,也是输出文件的名字。
- FILE_GUID:每个工程文件必须有一个 8-4-4-4-12 格式的 GUID,用于生成固件。
- VERSION_STRING:模块的版本号字符串。
- MODULE_TYPE:定义模块的类型,对于标准应用模块,设为 UEFI_APPLICATION
- ENTRY_POINT:定义模块的入口函数,根据源文件中的入口函数填写。
示例:
[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)
表示本块适用的体系结构,可以是 IA32、X64、IPF、EBC、ARM 中的一个。列出对应的[Sources.(Arch)]
,然后根据编译时标识设置,[Sources.$(Arch)]
中和标识相符的才会被编译。 - 编译工具链相关的源文件:有时文件后跟工具链名称,表示只有在该工具链编译器编译时才有效。
示例:
//体系结构相关块示例
[Sources]
Common.c
[Sources.IA32]
Cpu32
[Sources.X64]
Cpu64
//编译工具链相关的源文件示例
[Sources]
TimerWin.c | MSFT
TimerLinux.c | GCC
[Packages]
[Packages] 块中列出了本模块引用到的所有包的包声明文件(.dec)。
- 语法:每一行列出一个文件,文件使用相对 EDK2 的路径。若 [Sources] 列出了源文件,则 [Packages] 块必须列出 MdePkg/MdePkg.dec,并放在本块首行。
[LibraryClasses]
[LibraryClasses] 块列出本模块要链接的库模块。
- 语法:每一行声明一个要链接的库。
- 常用库:应用程序工程模块必须链接 UefiApplicationEntryPoint 库;驱动模块必须链接 UefiDriverEntyrPoint 库。
[Protocols]
[Protocols] 块列出模块中使用的 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 文件中。
- Arch:体系结构,可以是 IA32、X64、IPF、EBC、ARM、*。
- CC|DLINK:CC 表示编译选项,DLINK 表示连接选项。
- =|==:= 表示选项附加到默认选项后,== 表示仅使用定义的选项,弃用默认选项。
示例:(该选项可以避免一些无关紧要的警告在 EDK2 编译模块文件时作为错误)
[BuildOptions]
MSFT:*_*_*_CC_FLAGS = /w
3.1.3 标准应用程序加载过程
应用程序编译过程:
- UefiMain.c 首先被编译成目标文件 UefiMain.obj。
- 连接器将目标文件 uefiMain.obj 和其他库连接成 UefiMain.dll。
- GenFw 工具将 UefiMain.dll 转换成 UefiMain.efi。
连接器在生成 UefiMain.dll 时使用了 /dll/entry:_ModuleEntryPoint。.efi 是遵循 PE32 格式的二进制文件,_ModuleEntryPoint 便是这个二进制文件的入口函数。
将 UefiMain.efi 文件加载到内存
当 shell 中执行 UefiMain.efi 时,shell 首先用 gBS->LoadImage() 将UefiMain.efi 文件加载到内存生成 Image 对象,然后调用 gBS->StartImag(Image) 启动这个 Image 对象。StartImage 主要作用是找出可执行程序 Image 的入口函数并执行。gBS->StartImag() 是一个函数指针,指向 CoreStartImage 函数。
进入映像入口函数
CoreStartImage 的主要作用就是调用映像的入口函数。gBS->StartImage 的核心是Image->EntryPoint(···),它就是程序映像的入口函数,对应程序来说就是 _ModuleEntryPoint 函数。进入 _ModuleEntryPoint 后,控制权才转交给应用程序(UefiMain.efi)。
_ModuleEntryPoint 部分代码如下:
EFI_STATUS
EFIAPI
_ModuleEntryPoint (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
if (_gUefiDriverRevision != 0) {
// Make sure that the EFI/UEFI spec revision of the platform is >= EFI/UEFI spec revision of the application.
if (SystemTable->Hdr.Revision < _gUefiDriverRevision) {
return EFI_INCOMPATIBLE_VERSION;
}
}
// Call constructor for all libraries.
ProcessLibraryConstructorList (ImageHandle, SystemTable);
// Call the module's entry point
Status = ProcessModuleEntryPointList (ImageHandle, SystemTable);
// Process destructor for all libraries.
ProcessLibraryDestructorList (ImageHandle, SystemTable);
// Return the return status code from the driver entry point
return Status;
}
_ModuleEntryPoint 主要处理三件事:
- 初始化:初始化函数 ProcessLibraryConstructorList 中调用一系列构造函数。
- 调用本模块的入口函数:ProcessModuleEntryPointList 中调用的是工程模块定义的入口函数。
- 析构:ProcessLibraryDestructorList 中调用一系列析构函数。
进入模块入口函数
在 ProcessModuleEntryPointList 函数中调用了工程模块的真正入口函数UefiMain。
标准应用程序工程模块入口函数调用的整个过程:StartImage -> _ModuleEntryPoint -> ProcessModuleEntryPointList -> UefiMain。
3.2 其他类型工程模块
3.2.1 Shell 应用程序工程模块
源文件
Shell 应用程序工程模块以 INTN ShellAppMain(In UINTN Argc, IN CHAR16**Argv) 作为入口函数。
#include <Uefi.h>
#include <Library/UefiBootServicesTableLib.h>
INTN ShellAppMain(IN UINTN Argc, IN CHAR16 **Argv)
{
gST->ConOut->OutputString (gST->ConOut,L"HelloWorld\n");
return 0;
}
第一个参数 Agrc 是命令行参数个数,第二个参数 Argv 是命令行参数列表,列表中每个参数都是 Unicode 字符串。
工程文件
- [Defines] 块基本与标准应用程序工程模块相同,除了 ENTRY_POINT,必须设置为 ShellAppMain。
- [Packages] 块中必须列出 MdePkg/MdePkg.dec 和 ShellPkg/ShellPkg.dec。
- [LibraryClasses] 块中必须列出 ShellCEntryLib,通常还要列出 UefiBootServicesTableLib 和 UefiLib。
示例:
[Defines]
INF_VERSION = 0x00010006
BASE_NAME = Main
FILE_GUID = 4ea97c46-7491-4dfd-b442-747010f3ce5f
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 1.0
ENTRY_POINT = ShellCEntryLib
[Sources]
Main.c
[Packages]
MdePkg/MdePkg.dec
ShellPkg/ShellPkg.dec
[LibraryClasses]
ShellCEntryLib
UefiBootServicesTableLib
UefiLib
3.2.2 使用 main 函数的应用程序工程模块
源文件
#include <stdio.h>
int main(IN int Argc,IN char **Argv)
{
printf("Hello World!\n");
return(0);
}
工程文件
- [Defines] 块中设置 ENTRY_POINT 为 ShellCEntryLib。
- [Packages] 块中列出 MdePkg/MdePkg.dec、ShellPkg/ShellPkg.dec、StdLib/StdLib.dec。
- [LibraryClasses] 块中列出 ShellCEntryLib、LibC、LibStdio。
示例:
[Defines]
INF_VERSION = 0x00010006
BASE_NAME = Main
FILE_GUID = 4ea97c46-7491-4dfd-b442-747010f3ce5f
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 1.0
ENTRY_POINT = ShellCEntryLib
[Sources]
Main.c
[Packages]
MdePkg/MdePkg.dec
ShellPkg/ShellPkg.dec
StdLib/StdLib.dec
[LibraryClasses]
LibC
LibStdio
ShellCEntryLib
3.2.3 库模块
在传统 C/C++ 项目开发中经常会用到库,EDK2 也提供库模块。
工程文件
- [Defines] 块中 MODULE_TYPE 设置为 BASE。
- [Defines] 块中 LIBRARY_CLASS 设置为库名字,同时不需要设置 ENTRY_POINT。
- [Packages] 块中列出库引用到的包。
- [LibraryClasses] 块中列出包所依赖的其他库。
示例:(zlib 库的工程文件)
[Defines]
INF_VERSION = 0x00010006
BASE_NAME = zlib
FILE_GUID = 4ea97c46-7491-4dfd-b442-747010f3ce5f
MODULE_TYPE = BASE
VERSION_STRING = 1.0
LIBRARY_CLASS = zlib
[Sources]
adler32.c
.
.
.
[Packages]
MdePkg/MdePkg.dec
MdeModulePkg/MdeModulePkg.dec
StdLib/StdLib.dec
[LibraryClasses]
MemoryAllocationLib
BaseLib
UefiBootServicesTableLib
BaseMemoryLib
UefiLib
有些库只能被某些特定的模块调用,需在工程文件中声明库的适用范围,格式如下:
LIBRARY_CLASS = 库名称 | 适用模块类型1 适用模块类型 2
编写好库后,要使库能被其它模块调用,还要在包的 .dsc 文件中声明该库。在[LibraryClasses] 块中添加库模块的工程文件路径。
[LibraryClasses]
zlib | zlib/zlib.inf
调用库模块时,需要在调用模块的工程文件中添加被调用的库模块的库名。
[LibraryClasses]
zlib
3.2.4 UEFI 驱动模块
在 UEFI 中,驱动分为两类:符合 UEFI 驱动模型的驱动,模块类型为 UEFI_DRIVER,称为 UEFI 驱动;不遵循 UEFI 驱动模型的驱动,称为 DXE 驱动。
源文件
驱动与应用程序的模块入口函数类型一样,原型如下所示:
typedef EFI_STATUS API (*UEFI_ENTRYPOINT)(
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable);
驱动与应用程序的最大区别是驱动会常驻内存,而应用程序执行完毕后就会从内存清除。
工程文件
- [Defines] 块中将 MODULE_TYPE 设置为 UEFI_DRIVER。
- [Sources] 块中通常包含 ComponentName.c,该文件中定义了驱动的名字,驱动安装之后,这个名字将显示给用户。
- [LibraryClasses] 块中必须包含 UefiDriverEntryPoint。
示例:
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = DiskIoDxe
MODULE_UNI_FILE = DiskIoDxe.uni
FILE_GUID = 6B38F7B4-AD98-40e9-9093-ACA2B5A253C4
MODULE_TYPE = UEFI_DRIVER
VERSION_STRING = 1.0
ENTRY_POINT = InitializeDiskIo
[Sources]
ComponentName.c
DiskIo.h
DiskIo.c
[Packages]
MdePkg/MdePkg.dec
[LibraryClasses]
UefiBootServicesTableLib
UefiDriverEntryPoint
MemoryAllocationLib
BaseMemoryLib
BaseLib
UefiLib
DebugLib
[Protocols]
gEfiDiskIoProtocolGuid ## BY_START
gEfiBlockIoProtocolGuid ## TO_START
3.3 包及 .dsc、.dec、.fdf 文件
UEFI 的包中都会有一个 .dsc 文件和一个 .dec 文件。
- build 命令用于编译包,需要一个 .dsc 文件、一个 .dec 文件和一个或多个 .inf 文件。
- GenFW 命令用于制作固件或 Option Rom Image,需要一个 .dec 文件和一个 .fdf 文件。
3.3.1 .dsc 文件
.inf 用于编译一个模块,而 .dsc 用于编译一个 Package。它包含几个必需部分:[Defines]、[LibraryClasses]、[Components] 和可选部分:[PCD]、[BuildOptions] 等。
[Defines]
[Defines] 用于设置 build 相关的全局变量,这些变量可以被 .dsc 文件的其他模块引用,必须是 .dsc 文件的第一部分。格式如下:
[Defines]
宏变量名 = 值
DEFINE 宏变量名 = 值
EDK_GLOBAL 宏变量名 = 值
[Defines] 中通过 DEFINE 和 EDK_GLOBAL 定义的宏可以在 .dsc 文件和 .fdf 文件中通过 $(宏变量名)
使用。
示例:
[Defines]
PLATFORM_NAME = Shell
PLATFORM_GUID = E1DC9BF8-7013-4c99-9437-795DAA45F3BD
PLATFORM_VERSION = 1.0
DSC_SPECIFICATION = 0x00010006
OUTPUT_DIRECTORY = Build/Shell
SUPPORTED_ARCHITECTURES = IA32|IPF|X64|EBC|ARM|AARCH64
BUILD_TARGETS = DEBUG|RELEASE
SKUID_IDENTIFIER = DEFAULT
[LibraryClasses]
[LibraryClasses] 块定义了库的名字和库 .inf 文件的路径,这些库可以被 [Components] 块内的模块引用。语法:
[LibararyClasses.$(Arch).$(MODULE_TYPE)]
LibraryName | path/LibraryName.inf
[LibararyClasses.$(Arch1).$(MODULE_TYPE1), LibararyClasses.$(Arch1).$(MODULE_TYPE1)]
LibraryName | path/LibraryName.inf
-
$Arch
和$MODULE_TYPE
是可选项。不使用表示通用。 -
$Arch
表示体系结构,可以是下列值之一:IA32、X64、IPF、EBC、ARM、common。common 表示对所有体系结构有效。 -
$MODULE_TYPE
表示模块的类别,块内列出的库只提供$(MODULE_TYPE)
类别的模块连接。它可以是下列值:SEC、PEI_CORE、PEIM、DXE_CORE、DXE_SAL_DRIVER、BASE、DXE_SMM_DRIVER、DXE_DRIVER、DXE_RUNTIME_DRIVER、UEFI_DRIVER、UEFI_APPLICATION、USER_DEFINED。
示例:
[LibraryClasses.common]
UefiApplicationEntryPoint|MdePkg/Library/UefiApplicationEntryPoint/UefiApplicationEntryPoint.inf
···
[LibraryClasses.ARM]
NULL|ArmPkg/Library/CompilerIntrinsicsLib/CompilerIntrinsicsLib.inf
···
[LibraryClasses.AARCH64]
NULL|ArmPkg/Library/CompilerIntrinsicsLib/CompilerIntrinsicsLib.inf
···
[Components]
[Components] 块内定义的模块都会被 build 工具编译并生成 .efi 文件,格式如下:
[Components]
path\Exectuables.inf
[Components]
path\Exectuables.inf{
<LibraryClasses> # 嵌套块
LibraryName | Path/LibraryName.inf
BuildOptions> # 嵌套块
#子块中还可以包含< Pcds*>
}
path 使用相对于EDK2根目录的相对路径。
[BuildOptions]
[BuildOptions] 块和 .inf 中的用法相同。
[PCD]
[PCD] 块用于定义平台配置数据,其目的是在不改动 .inf 文件和源文件的情况下完成对平台的配置。
例如:
在 UEFI 模拟器 Nt32Pkg 的 Nt32Pkg.dsc 文件中,可以通过 PCD 的 PcdWinNtFileSystem 来配置模拟器的文件系统路径。如下所示:
gEFINt32PkgTokenSpaceGuid.PcdWinNtFileSystem|L".!..\..\..\..\EdkShellBinPkg\Bin\Ia32\Apps"|VOID*|106
“|”将配置分为了四个部分,第一部分 gEFINt32PkgTokenSpaceGuid 是名字空间,第二部分是值,第三部分是变量类型,第四部分是变量数据的最大长度。
在源文件中可以使用 LibPcdGetPtr(_PCD_TOKEN_PcdWinNtFileSystem) 获得 gEfiNt32PkgTokenSpaceGuid.PcdWinFileSyste 定义的值。
3.3.2 .dec 文件
.dec 文件定义了公开的数据和接口,供其他模块使用。包含了必需区块 [Defines] 和可选区块 [Includes]、[LibraryClasses]、[Guids]、[Protocol]、[Ppis]、[PCD] 几个部分。
[Defines]
[Defines] 块用于提供 Package 的名称、GUID、版本号等信息。
示例:
[Defines]
DEC_SPECIFICATION = 0x00010005
PACKAGE_NAME = MdePkg
PACKAGE_GUID = 1E73767F-8F52-4603-AEB4-F29B510B6766
PACKAGE_VERSION = 1.03
[Includes]
[Includes] 块内列出了本 Package 提供的头文件所在目录。
示例:
[Includes]
Include
[Includes.IA32]
Include/Ia32
[Includes.X64]
Include/X64
[LibraryClasses]
Package 可以通过 .dec 文件对外提供库,每个库都必须有一个头文件,放在 Include\Library 目录下。[LibraryClasses] 块用于明确库和头文件的对应关系。
示例:
[LibraryClasses]
UefiUsbLib|Include/Library/UefiUsbLib.h
[LibraryClasses.IA32, LibraryClasses.X64]
SmmLib|Include/Library/SmmLib.h
[Guids]
在 Package\Include\Guid 目录中有很多文件,每个文件内定义了一个或几个 GUID。这些定义只是声明,常量真正定义在 AutoGen.c 中,它的值定义在 .dec 文件的 [Guids] 区块。
示例:
[Guids]
## Include/Guid/Gpt.h
gEfiPartTypeLegacyMbrGuid = { 0x024DEE41, 0x33E7, 0x11D3, { 0x9D, 0x69, 0x00, 0x08, 0xC7, 0x81, 0xF3, 0x9F }}
## Include/Guid/Gpt.h
gEfiPartTypeSystemPartGuid = { 0xC12A7328, 0xF81F, 0x11D2, { 0xBA, 0x4B, 0x00, 0xA0, 0xC9, 0x3E, 0xC9, 0x3B }}
## Include/Guid/Gpt.h
gEfiPartTypeUnusedGuid = { 0x00000000, 0x0000, 0x0000, { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }}
当在模块工程文件的 [Guids] 中引用这些 Guid 时,这些值就会复制到 AutoGen.c 中。
[Protocols]
在 Package\Include\Protocols 目录下有很多头文件,每个头文件定义了一个或多个 Protocol,这些 Protocol 的 GUID 值就定义在 .dec 文件的 [Protocols] 区块。
示例:gEfiBlockIoProtocolGuid 的值就定义在 MdePkg.dec 的 [Protocols] 块内。
[Protocols]
gEfiBlockIoProtocolGuid = { 0x964E5B21, 0x6459, 0x11D2, { 0x8E, 0x39, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B }}