UEFI 工程模块文件

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.hUefi.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(映像)ImageHandleImage 对象句柄,作为模块入口函数参数,它表示模块自身加载到内存后生成的 Image 对象
SystemTable 是程序同 UEFI 内核交互的桥梁,通过它可以获得 UEFI 提供的各种服务,如启动(BT)服务和运行时(RT)服务。SystemTable 是 UEFI 内核中的一个全局结构体

向标准输出设备打印字符串是通过 SystemTableConOut 提供的服务完成的。
ConOutEFI_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) 是可选项,可以是IA32X64IPFEBCARM 中的一个,表示本块适用的体系结构。[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.efiShell 首先用 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.hAutoGen.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函数中会调用StdLibShellAppMain(..),这个 ShellAppMain函数会对 StdLib进行初始化。StdLib的初始化完成后才可以调用 StdLib的函数。

通常,使用main函数的应用程序工程模块在AppPkg环境下才能成功编译。首先将main.inf添加到 AppPkg\AppPkg.dsc 文件的[Components]

3. 库模块

在库模块的工程文件中,需要设置MODULET_YPEBASE;设置LIBRARY_CLASSlibrary的名字;同时,不要设置 ENTRY_POINT
[Packages]块列出库引用到的包,[LibraryClasses]列出包所依赖的其他库。

有些库仅能被某些特定的模块调用,编写这种库时需在工程文件中声明库的适用范围声明方法是在[Defines]块的 LIBRARY_CLASS 变量中定义,格式如下:

LIBRARY_CLASS = 库名字 | 适用模块类型1  适用模块类型2

编写了库之后,要使库能被其他模块调用,还要在包的.dsc文件中声明该库。

例如要使得 AppPkg 中的模块能调用 zlib 库,需将 zlib|zlib-1.2.6/zlib.inf(假设 zlib-1.2.6EKD2的根目录下)放到AppPkg\AppPkg.dsc文件[LibraryClasses]中,调用 zlib 时,在需要链接 zlib 的工程模块的工程文件 [LibraryClasses]中添加 zlib 即可。

如果库使用之前需要进行初始化,在库的工程文件需指定 CONSTRUCTORDESTRUCTORCONSTRUCTOR 函数会加入到ProcessLibraryConstructorList 中,这个 CONSTRUCTOR 函数会在 ENTRY_POINT 之前执行;DESTRUCTOR 函数会加入到ProcessLibraryDestructorList中,这个 DESTRUCTOR 就会在ENTRY_POINT后执行。

4. UEFI 驱动模块

在 UEFI 中,驱动分为两类:一类是符合 UEFI 驱动模型的驱动,模块类型为 UEFI_DRIVER 称为 UEFI驱动;另一类是不遵循 UEFI 驱动模型的驱动,模块类型包括 DXE_DRIVERDXE_SAL_DRIVERDXE_SMM_DRIVERDXE_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] 中通过DEFINEEDK_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,这些 ProtocolGUID值就定义在.dec 文件的[Protocols]区块。

内容基本全部来自于 《UEFI原理与编程》、、

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

飞大圣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值