美文网首页
第三章 UEFI 工程模块文件

第三章 UEFI 工程模块文件

作者: Maxwell_Li | 来源:发表于2018-09-05 11:20 被阅读0次

    作者: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 工程文件

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

    工程文件块.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 <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 文件中通过 $(宏变量名) 使用。

    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{
      <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 }}
    

    相关文章

      网友评论

          本文标题:第三章 UEFI 工程模块文件

          本文链接:https://www.haomeiwen.com/subject/oncrwftx.html