UEFI 工程模块文件-标准应用程序工程模块
前言
在EDK2环境下编程之前,先介绍EDK2的两个概念模块(Module)和包(Package).
“包”是一组模块及平台描述文件(.dsc文件)、包声明文件(.dec文件)则、组成的集合,多在以*pkg命名的文件夹中,一般也称这样的文件夹为一个包。
模块是UEFI系统的一个特色。模块(可执行文件,即.efi文件)像插件一样可以动态地加载到UEFI内核中。对应到源文件,EDK2中的每个工程模块由元数据文件(.inf)和源文件(有些情况也可以包含.efi文件)组成。
主要介绍3种应用程序模块、UEFI驱动模块和库模块。
一、标准应用程序工程模块
标准引用程序工程模块是其它应用程序模块的基础,也是UEFI中常见的一种应用程序模块。每个工程模块由两部分组成:工程文件和源文件。源文件包括C/C++文件、.asm汇编文件,也可以包括.uni(字符串资源文件)和.vrf(窗体资源文件)等资源文件。
1.源文件
示例程序:
//hello.c
#include<Uefi.h>
EFI_STATUS UefiMain(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable)
{SystemTable->ConOut->OutputString(SystemTable->ConOut,L"Hello man,\n welcome to UEFI world\n");return EFI_SUCCESS;
}
- 头文件:所有的标准应用程序工程模块的源文件的头文件都要包含Uefi.h。Uefi 定义了UEFI基本数据类型和核心数据结构。
- 入口函数:UEFI标准应用程序的入口函数通常是
UefiMain
,它是约定成俗的函数,它是可以在.inf
文件中指定。它的函数签名(返回值类型和参数列表类型)是不能变化的。
- 入口函数的返回值类型是
EFI_STATUS
。
- 在UEFI程序中基本所有的返回值类型都是EFI_STATUS。它的本质是无符号长整数
- 最高位为1时其值为错误代码,为0时表示非错误值。通过宏EFI_ERROR(Status)可以判断返回值Status时候为错误代码。若Status为错误代码EFI_ERROR(Status)返回值为真,否则为假。
- EFI_SUCCESS为预定义常量,其值为0,表示没有错误的状态值和返回值。
- 入口函数的返回值类型是
入口函数参数
ImageHandle
和SystenTable
- .efi文件(UEFI应用程序或UEFI驱动程序)加载到内存后生成的对象成为
Image
(映像)。ImageHandle
是Image
的句柄,作为模块入口函数的参数,它表示模块自身加载到内存后生成的Image
对象 。 SystemTable
是程序同UEFI内核交互的桥梁,通过它可以获得UEFI提供的各种服务(BT/RT),SystemTable
是UEFI内核的一个全局结构体。
向标准输出设备打印字符串是通过
SystemTable
的ConOut
提供的OutputString
服务完成的。ConOut
是EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
的一个实例,而EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
主要功能是控制字符输出设备。OutputString
服务的第一个参数是指向EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL
的一个实例即:ConOut
。第二个参数是Unicode字符串。简单讲就是SystemTable->ConOut->OutputString
服务将字符串L”Hello World”打印到SystemTable->ConOut
所控制的字符串输出设备。- .efi文件(UEFI应用程序或UEFI驱动程序)加载到内存后生成的对象成为
2.工程文件
工程模块分为很多块,每个块以“[快名]”开头,它必须单独占一行。有些块是所有工程文件都必需的块,这些块包括[Defines]、[Sources]、[Packages]和[LibraryClass]。
详细的工程模块如下表:
[Defines]块
[Defines]块用于定义模块的属性和其它变量,块内定义的变量可以被其它块引用。- 属性定义的语法
属性名 = 属性值 - 块内必须属性
- INF_VERSION:INF标准版本号。EDK2的build会检查INF_VERSION 的值并根据这个值解释
.inf
文件。设置为0x00010006或0x00010005。 - BASE_NAME:模块名字符串,不能包含空格。它通常也是输出文件的名字。
- FILE_GUID:每个工程文件必须有一个
8-4-4-4-12
格式的GUID用于生成固件。(每一位数十六进制 0-F) - VERSION_STRING:模块的版本号,一般设置为
1.0
,根据自己写的模块版本设定即可。 - MODULE_TYPE:定义模块的模块类型,对于标准应用模块,设为UEFI_APPLICATION.
- ENTRY_POINT:定义模块的入口函数,根据在源文件中的入口函数填写。一般是UefiMain。
- INF_VERSION:INF标准版本号。EDK2的build会检查INF_VERSION 的值并根据这个值解释
- 属性定义的语法
[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)
,其中$(Arch)
是表示本块的体系结构,可以是IA32
,X64
,IPF
,EBC
,ARM
中一个。这个的作用是不同的体系结构可能包含的源文件或资源文件不同,如果都写进[Sources]
可能有问题,但是可以列出对应的[Sources.$(Arch)]
,然后根据编译时标识设置,[Sources]
都会被编译,[Sources.$(Arch)]
中和标识相符的才会被编译。 - 编译工具链相关的源文件
有是文件后跟工具链的符号表示只有在该工具链编译器编译时有效。
- MSFT : Visual Stdio
- INTEL : ICC编译器
- RVCT : ARM编译器
- 语法
//体系结构相关块示例
[Sources]
Common.c
[Sources.IA32]
Cpu32
[Sources.X64]
Cpu64
//编译工具链相关的源文件示例
[Sources]
TimerWin.c | MSFT
TimerLinux.c | GCC
- [Packages]块
[Packages]列出本模块引用到的所有包的声明(.dec)文件。
- 语法
[Packages]块内每一行列出一个文件,文件使用相对于EDK2根目录的路径。若[Sources]列出了源文件,则[Packages]块必须列出MdePkg/MdePkg.dec,并将其放在本块首行。
- 语法
- [LibraryClasses]
[LibraryClasses]块列出本模块要连接的库模块。
- 语法
块内每一行声明一个要连接的库(库的定义在.dsc文件中) - 常用库
应用程序工程模块必须连接UefiApplicationEntryPoint库,驱动模块必须连接UefiDriverEntryPoint库。
- 语法
非必须块(如果有用到,则需要写出)
- [Protocols]块
[Protocol]列出的模块中使用的Protocol,实际上是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文件中,与定义编译器名字:VS2003,VS2005,VS2008,VS2010,GCC44,GCC45,GCC46,CYGGCC,ICC等 ,*表示对指定家族的编译器都有效。
- Arch是体系结构,与前述相同,可以是
IA32
,X64
,IPF
,EBC
,ARM
中一个,* 对所有体系结构有效。 - CC表示编译选项,DLINK表示连接选项
- =表示选项附加到默认选项后面,==表示仅使用所定义的选项,弃用默认选项
- =,==后面接选项
- 语法
注:
这是个很有用的选项,我们写正常C程序时一些无关紧要的警告在EDK2编译模块文件时会将它是做错误。所以可以使用下面的[BuildOptions]
可以避免将这些警告堪称错误。
[BuildOptions]
MSFT:*_*_*_CC_FLAGS = /w
下面是HelloWorld的.inf
文件
// HelloWorld.inf
[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]HelloWorld.c[Packages]MdePkg/MdePkg.dec[LibraryClasses]UefiApplicationEntryPointUefiLib[BuildOptions]
MSFT:*_*_*_CC_FLAGS = /w
三、编译运行
1. 添加工程文件
将工程文件即HelloWorld.inf 添加到NT32Pkg.dsc的[Components]部分。
2. 加载EDK2环境
打开MS-DOS(cmd),进入到EDK2的根目录,用edksetup.bat
加载EDK2环境。
3. 输入
build -p Nt32Pkg\Nt32Pkg.dsc -m [helloworld.inf相对于EDK2根目录的相对路径名] -a IA32
(64位用X64)
4. build 信息
build 成功后画面如下:
输出内容里也会指明文件输出的路径。
5. 运行
在MS-DOS输入 build run
进入UEFI模拟环境,运行前面目录下的HelloWorld.efi文件。
注:可以输入fs0:
快速进入/EDK2/Build/NT32IA32/DEBUG_VS2008/IA32/
6. 运行结果
四、标准应用程序加载过程
编译过程:
1. HelloWorld.c 首先被编译成目标文件 HelloWorld.obj
2. 连接器将目标文件HelloWorld.c 和其它库连接成HelloWorld.dll。
3. GenFw 工具将HelloWorld.dll 转化成 HelloWorld.efi。
上述过程由 build
命令自动完成,连接器在生成HelloWorld.dll时使用了/dll/entry:_ModuleEntryPoint。.efi是遵循了PE32格式的二进制文件,_ModuleEntryPoint便是这个二进制文件的入口函数。下面探讨应用程序加载过程,主要看_ModuleEntryPoint和源文件中入口函数UefiMain的关系。
1. 将HelloWorld.efi 文件加载到内存
当shell中执行HelloWorld.efi时,shell首先用gBS->LoadImage()将HelloWorld.efi文件加载到内存生成Image对象,然后调用gBS->StartImag(Image)启动这个Image对象。gBS->StartImage()是一个函数指针,它实际指向的是CoreStartImage()
2. 进入映像入口函数
CoreStartImage()的主要作用是调用映像入口函数,在gBS->StartImage 的核心是Image->EntryPoint(···),它就是程序映像的入口函数,对应程序来说就是_ModuleEntryPoint 函数。进入 _ModuleEntryPoint 后,控制权才转交给应用程序(HelloWorld.efi)。
_ModuleEntryPoint主要处理三件事:
1. 初始化:初始化函数ProcessLibraryConstructorList中调用一系列构造函数
2. 调用本模块的入口函数 : ProcessModuleEntryPointList 中调用的是工程模块定义的入口函数
3. 析构:ProcessLibraryDestructorList 中调用一系列析构函数。
这三个对应的函数AutoGen.h,AutoGen.c中。
3. 进入模块入口函数
在ProcessModuleEntryPointList函数中调用了工程模块的真正入口函数UefiMain。
五、总结
标准应用程序模块是其它应用程序模块的基础,需要对它熟悉使用掌握,后续会接着介绍其它类型的工程模块。
另外,此篇文章后的demo是在已经安装了EDK2环境基础上编译运行的。如果还没有安装,可以参考:UEFI原理与编程(一):环境搭建。
参考资料
<1>《UEFI原理与编程》戴正华 著
<2> UEFI Spec2_6
<3> 百度百科