第三章 UEFI 工程模块文件

作者:Maxwell Li
日期:2017/12/05
未经作者允许,禁止转载本文任何内容。如需转载请留言。


[TOC]

“包”是一组模块及平台描述文件(.dsc)、包声明文件(.dec)组成的集合。
模块(.efi)像插件一样可以动态地加载到 UEFI 内核中。
在 EDK2 环境下,除了要编写源文件外,还要为工程编写元数据文件(.inf)。

3.1 标准应用程序工程模块

标准引用程序工程模块是其它应用程序模块的基础,也是UEFI中常见的一种应用程序模块。每个工程模块由两部分组成:工程文件和源文件。

3.1.1 源文件

示例程序:

#include
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 工程文件

工程文件分为很多块,详细的工程模块如下所示:

第三章 UEFI 工程模块文件_第1张图片
工程文件块.png

[Defines]

[Defines] 块用于定义模块的属性和其他变量,块内定义的变量可以被其他块应用。

  • 语法:属性名 = 属性值
  • 块内必须属性:
  1. INF_VERSION:INF 标准版本号。EDK2 的 build 会检查 INF_VERSION 的值并根据这个值解释 .inf 文件。
  2. BASE_NAME:模块名字字符串,也是输出文件的名字。
  3. FILE_GUID:每个工程文件必须有一个 8-4-4-4-12 格式的 GUID,用于生成固件。
  4. VERSION_STRING:模块的版本号字符串。
  5. MODULE_TYPE:定义模块的类型,对于标准应用模块,设为 UEFI_APPLICATION
  6. 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[=|==]选项
  1. 编译器家族:MSFT、INTEL、GCC、RVCT。
  2. Target:DEBUG、RELEASE、 为通配符。
  3. TOOL_CHAIN_TAG:编译器名字,定义在 Conf/tools_def.txt 文件中。
  4. Arch:体系结构,可以是 IA32、X64、IPF、EBC、ARM、*。
  5. CC|DLINK:CC 表示编译选项,DLINK 表示连接选项。
  6. =|==:= 表示选项附加到默认选项后,== 表示仅使用定义的选项,弃用默认选项。

示例:(该选项可以避免一些无关紧要的警告在 EDK2 编译模块文件时作为错误)

[BuildOptions]
  MSFT:*_*_*_CC_FLAGS = /w

3.1.3 标准应用程序加载过程

应用程序编译过程:

  1. UefiMain.c 首先被编译成目标文件 UefiMain.obj。
  2. 连接器将目标文件 uefiMain.obj 和其他库连接成 UefiMain.dll。
  3. 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 主要处理三件事:

  1. 初始化:初始化函数 ProcessLibraryConstructorList 中调用一系列构造函数。
  2. 调用本模块的入口函数:ProcessModuleEntryPointList 中调用的是工程模块定义的入口函数。
  3. 析构:ProcessLibraryDestructorList 中调用一系列析构函数。

进入模块入口函数

在 ProcessModuleEntryPointList 函数中调用了工程模块的真正入口函数UefiMain。

标准应用程序工程模块入口函数调用的整个过程:StartImage -> _ModuleEntryPoint -> ProcessModuleEntryPointList -> UefiMain。

3.2 其他类型工程模块

3.2.1 Shell 应用程序工程模块

源文件

Shell 应用程序工程模块以 INTN ShellAppMain(In UINTN Argc, IN CHAR16**Argv) 作为入口函数。

#include 
#include 
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 
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 文件中通过 $(宏变量名) 使用。

第三章 UEFI 工程模块文件_第2张图片
dsc宏变量.png

示例:

[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{
   # 嵌套块
      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 }}

你可能感兴趣的:(第三章 UEFI 工程模块文件)