ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

UEFI原理与编程第二章学习-UEFI标准应用工程模块文件介绍及编译流程

UEFI原理与编程第二章学习-UEFI标准应用工程模块文件介绍及编译流程 标准应用程序工程模块标准应用程序工程模块是其他应用程序工程模块的基础也是UEFI中常见的一种应用程序工程模块。每个工程模块分为两部分工程文件和源文件标准应用程序工程模块也不例外。其中源文件包括C/C文件、.asm汇编文件也可以包括.uni字符串资源文件和 .vfr窗口资源文件等资源文件。一个简单的标准应用程序工程模块应该包含一个C程序模块本例中为Main.c以及一个工程文件Main.inf接下来逐部分编写一个简单的标准应用程序的各个模块。1、入口函数//简单的标准应用程序 #includeUefi,h EFI_STATUS UefiMain(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable) { SystemTable-ConOut-OutputString(SystemTable-ConOut, LHelloWorld\n); return EFI_SUCCESS; }一般来说标准应用程序至少要包含以下两个部分头文件所有的UEFI程序都要包含头文件Uefi.h。Uefi.h定义了UEFI基本数据类型以及核心数据结构。入口文件UEFI标准应用程序入口函数可以由开发者自行指定但通常命名为UefiMain。入口函数由工程文件UefiMain.inf指定。虽然入口函数的函数名可以变化但其函数签名即返回值类型和参数列表类型不能变化。入口函数返回值与参数讲解入口函数的返回值类型是EFI_STATUS。在UEFI程序中基本所有返回值类型都是EFI_STATUS。EFI_STATUS本质上是无符号程序长整数型变量。在最高位为1时值为错误代码为0时表示没有错误的状态值或返回值。通过宏EFI_ERROR(Status)可以判断返回值Status是否为错误码。若Status是错误吗则EFI_ERROR的返回值为TRUE否则为False。EFI_SUCCESS为预定义常量其值为0表示没有错误的状态或是返回值。入口函数的参数是ImageHandle和SystemTable。.efi文件UEFI应用程序或UEFI驱动程序加载到内存后生成的对象称为Image映像。ImageHandle是Image对象的句柄作为模块入口函数它表示模块自身加载到内存后生产的Image对象。SystemTable是程序同UEFI内核交互的桥梁程序通过SystemTable可以获得UEFI提供的各种服务如启动BS服务和运行时RT服务。SystemTable时UEFI内核中的一个全局结构体。向标准输出设备打印字符串是通过SystemTable的ConOut提供的服务完成的。ConOut是EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL的一个实例。而EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL的主要功能是控制字符输出设备。向输出设备打印字符串是通过ConOut提供的OutputString服务完成的。该服务函数第一个参数是This指针指向EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL实列此处为ConOut本身第二个参数是Unicode字符串。这条打印语句的流程是通过SystemTable-ConOut-OutputString五福将字符串LHelloWorld打印到SystemTable-ConOut所控制的字符输出设备。2、工程文件在编写Main.c文件的同时需要编写.infModule Information File文件。.inf文件时模块的工程文件其作用类似于Visual Studio的.proj文件用于指导EDK2编译工具自动编译模块。工程文件分为很多个块每个块以“[块名]”开头“[块名]”必须单独占一行。有的块是所有工程文件都必需的块还有另一部分的块仅在用的时候需要编写。必须块块描述[Defines]定义本模块的属性变量及其他变量这些变量可在其他块中引用[Sources]列出本模块的所有源文件及资源文件[Packages]列出本模块引用到的所有包的包声明文件[LibraryClasses]列出本模块要链接的库模块非必须块块描述[Protocols]列出本模块用到的Protocol[Guids]列出本模块用到的GUID[Pcd]列出本模块用到的Pcd变量这些Pcd变量可被整个UEFI系统访问[PcdEx]列出本模块用到的Pcd变量这些Pcd变量可被整个UEFI系统访问[FixedPcd]列出本模块用到的Pcd编译期常量[FeaturePcd]用于列出本模块用到的Pcd常量[PatchPcd]写出的Pcd变量进本模块可以使用[BuildOptions]指定编译和链接选项[Defines]块[Defines]块用于定义模块的属性和其他变量块内定义的变量可被其他块引用。(1)属性定义的语法属性名属性值(2)属性块内必须定义的属性包括INF_VERSION INF标准的版本号。EDK2的build会检查INF_VERSION的值并根据这个值解释.inf文件。通常将INF_VERSION的值设置为0x00010005其中前半部分为主版本号后半部分为次版本号。BASE_NAME模块名字符串不能包含空格。它通常也是输出文件的名字。例如BASE_NAMEUefiMain则最终生成的文件为UefiMain.efi。FILE_GUID每个工程文件必须有一个8-4-4-4-12格式的GUID用于生产固件。例如FILE_GUID6987936E-GUID-UEFI-AE86-012345678987。VERSION_STRING模块的版本号字符串。例如可以设置为 VERSION_STRING1.0。MODULE_TYPE定义模板的模板类型可以是SEC、PEI_CORE、PEIM、DXE_CORE、DEX_SAL_DRIVER、UEFI_APPLICATION、BASE中的一个。对标准应用程序工程模块来说MODULE_TYPE的值为UEFI_APPLICATION。ENTRY_POINT定义模块的入口函数。模块的入口函数就是通过设置ENTRY_POINT的值进行配置的。[Defines]块示例代码[Defines] INF_VERSION 0x00010005 BASE_NAME UefiMain FILE_GUID 6987936E-GUID-UEFI-AE86-012345678987 MODULE_TYPE UEFI_APPLICATION VERSION_STRING 1.0 ENTRY_POINT UefiMain[Sources]块(1)语法块内每一行代表一个文件文件使用相对路径根路径是工程文件所在的目录。这次编写的简单标准应用程序工程模块仅含有一个源文件。 [Sources]块如下所示[Sources.$(Arch)] UefiMain.c(2)体系结构相关块.$(Arch)是可选项可以是IA32、X64、IPF、EBC、ARM中的一个表示本模块适用的体系结构。[Sources]块适用于任何体系结构。例如编译32位模块时即在build命令选项中选择使用 -a IA32选项工程将包含[Sources]和[Sources.IA32]中的源文件编译64位模块时工程将包含[Sources]和[Sources.X64]中的源文件。(3)示例示例包含了三个块[Sources]块、[Sources.IA32]块、[Sources.X64]块。在编译32位的模块时模块包含[Sources.IA32]中的Cpu32.c文件和[Sources]中的Common.c文件在编译64位系统时模块包含文件[Sources.X64]中的Cpu64.c和[Sources]中的Common.c。[Sources]块、[Sources.IA32]块、[Sources.X64]块示例[Sources] common.c [Sources.IA32] Cpu32.c [Sources.X64] Cpu64.c(4)编译工具链相关的源文件有时文件名后面会有一个“|”符号该符号后面会跟工具链名字如下例所示工具链相关的源文件[Sources] TimerWin,c | MSFT TimerLinux.c | GCC这表示TimerWin.c仅在使用 Visual Studio 编译器时有效TimerLinux.c仅在使用GCC编译器时有效。处理MSFT和GCC外EDK2还定义了INTEL和RVCT两种工具链。INTEL工具链是ICC编译器或Intel EFI 字节码编译器RVCT是ARM编译器。[Packages]块[Packages]块列出了本模块引用到的所有包的包声明文件类似C开头的import即.dec文件。(1)[Packages]语法[packages]块内每一行列出一个文件文件使用相对路径相对路径的根路径位EDK2的根目录。如果[Sources]块内列出了源文件则在[Packages]块内必须列出MdePkg/MdePkg。.dec,并将其放在本块的首行详见示例。(2)示例由于编写的简单标准应用工程模块示例 [Sources] 块中 UefiMain.c仅引用了MdePkg中的头文件Uefi.h因此[Packages]仅列出MdePkg/MdePkg.dec即可示例如下[Packages] MdePkg/MdePkg.dec[LibraryClasses]块[LibraryClasses]块的功能是列出本模块要链接的库模块。(1)语法块内每一行声明一个要链接的库库定义在包的 .dsc文件中语法如下[LibraryClasses]​ 库名称(2)常用库应用程序工程模块必须链接UefiApplicationEntryPoint库驱动模块必须链接UefiDriverEntryPoint库。(3)示例这次尝试编写的简单应用程序工程模块中UefiMain.c文件的UefiMain函数没有调用除UefiApplicationEntryPoint以外的其他库函数因此只需要在[LibraryClasses]块中列出UefiApplicationEntryPoint即可示例如下[LibraryClasses] UefiApplicationEntryPoint[Protocols]块[Protocols]块中列出的是在模块中使用到的Protocol对应的GUID。如果模块中未使用任何Protocol则[Protocols]块为空。(1)语法块内的每一行声明一个在本模块中引用的Protocol。语法格式如下[LibraryClasses]​ Protocol 的 GUID(2)示例如果在程序中使用了某个Protocol的GUID。例如源程序中使用了类似代码Status gBS-LocateProtocol ( gEfiHiiDatabaseProtocolGuid, NULL, (VOID**) HiiDatabsa);则在[Protocols]块中必须要列出gEfiHiiDatabaseProtocolGuid示例如下[LibraryClasses] gEfiHiiDatabaseProtocolGuid[BuildOptions]块[BuildOptions]块制定了本模块的编译和链接选项。(1)语法[BuildOptions] [编译器家族]: [$(Target)]_[TOOL_CHAIN_TAG]_[$(Arch)]_[CC|DLINK]_FLAGS [|] [选项]各选项说明如下[编译器家族]可以是MSFTVisual Studio编译器家族、INTELIntel编译器家族、GCCGCC编译器家族和RVCTARM编译器家族中的某一个。[Target]值可以是DEBUG、RELEASE和*中的任意一个*为通配符表示对DEBUG和RELEASE都有效。[TOOL_CHAIN_TAG]是编译器的名字。编译器的名字在Conf\target.txt文件中的第60行。预定的编译器名字可以是VS系列VS2010、VS2015、VS2017等、GCC系列GCC45、GCC46等、可以是CYGGCC或是ICC等还可以使用通配符*表示对所有编译家族内的所有编译器都适用。[Arch]是体系结构可以IA32、X64、IPF、EBC、ARM或*其中*表示对所有体系结构都有效。[CC|DLINK]CC表示编译选项。DLINK表示链接选项。[|]表示将选项附加到默认选项后表示仅使用所定义的选项弃用默认选项。[选项]此项为编译选项或连接选项。(2)示例示例6.1 使用’的[BuildOptions]块编写用Visual Studio编译器进行编译且在编译时添加/wd4804编译选项连接时添加/BASE:0选项。[BuildOptions] MSFT:*_*_*_CC_FLAGS /wd4804 MSFT:*_*_*_DLINK_FLAGS /BASE:0示例6.2 使用的[BuildOptions]块编写是使用Visual Studio编译器进行编译32位的DEBUG版本仅使用指定的编译选项并忽略所有默认的编译选项。[BuildOptions] MSTF:DEBUG_VS2010_IA32_CC_FLAGES /nologo /c /WX /GS- /W4 /Gs32768 /DUNICODE /Olib2 /GL /EHs-c- /GR- /GY /Zi /Gm /D EFI_SPECIFCATION_VERSION 0x0002000A /D TIANO_RELEASE_VERSION 0x00080006 /FAs /Oi-等上述代码块都编译好后将其放在一起组成完整的标准应用程序工程文件示例如下示例标准应用程序HelloWorld的完整工程文件[Defines] INF_VERSION 0x00010005 BASE_NAME UefiMain FILE_GUID 6987936E-GUID-UEFI-AE86-012345678987 MODULE_TYPE UEFI_APPLICATION VERSION_STRING 1.0 ENTRY_POINT UEFIMAIN [Sources] main.c [Packages] MdePkg/MdePkg.dec [LibraryClasses] UefiApplicationEntryPoint UefiLib3、编译和运行源文件和工程都编写完成后将UefiMain.inf的相对uefi路径添加到Nt32Pkg.dsc或UnixPkg.dsc的[Components]部分示例代码如下uefi在EDK2文件夹中[Components] uefi/book/infs/UefiMain.inf添加完成后打开BaseTools下的build工具进行编译即可Windows下执行下列命令进行编译C:\EDK2edksetup.bat --nt32 C:\EDK2build -p Nt32PkgNt32Pkg.dsc -a IA32||X64Linux下执行下例命令进行编译$source ./edksetup.sh BaseTools $source -p Unixpkg/UnixPkg.dsc -a IA32※4、标准应用程序的加载过程应用程序被编译成 .efi 文件1UefiMain.c首先被编译成目标文件UefiMain.obj。2连接器将目标文件UefiMain.obj和其他库连接成UefiMain.dll。3GenFw工具将UefiMain.dll转换成UefiMain.efi。以上整个过程由build命令自动完成2、3阶段执行的命令如下由图可以看出连接器在生产UefiMain.dll文件时使用了/dll/entry:_ModuleEntryPoint。.efi文件是遵循PE32格式的二进制文件_ModuleEntryPoint是这个二进制文件的入口。将UefiMain.efi文件加载到内存在Shell中执行UefiMain.efi时Shell首先用gBS-LoadImage()将UefiMain.efi文件加载到内存生成Image对象然后调用gBS-StartImage(Image)启动这个Image。具体加载过程代码如下//file ShellPkg\Application\Shell\ShellProtocol.cEFI_STATUS EFIAPIInternalShellExecuteDevicrPath(IN CONST EFI_HANDLE*ParentImageHandle,IN CONST EFI_DEVICE_PATH_PROTOCOL*DevicePath,//UefiMain.efi的设备路径IN CONST CHAR16*CommandLine OPTIONAL,//应用程序所需的命令行参数IN CONST CHAR16**Environmrnt OPTIONAL,//UEFI环境变量OUT EFI_STATUS*StatusCode OPTIONALa//程序UefiMain.efi的返回值){//定义参数EFI_STATUS Status;EFI_HANDLE NewHandle;EFI_LOADED_IMAGE_PROTOCOL*LoadedImage;LIST_ENTRY OrigEnvs;EFI_SHELL_PARAMETERS_PROTOCOL ShellParamsProtocol;.../* 第一步将UefiMain.efi文件加载到内存生成Image对象NewHandle为其句柄 句柄 1、特殊的智能指针当一个应用程序要引用其他系统管理的内存块或对象时 2、Windows编程的基础。句柄指的是使用一个唯一的4字节型整数值来标识应用程序中 的不同对象和同类中的不同示例。应用程序能通过句柄访问相应对象的信息但这种句柄 不是指针应用程序不能通过句柄直接阅读文件中的信息。句柄是Windows系统用来标识 应用程序中建立的或是使用的资源的唯一整数。 使用gBS-LoadImage()函数将加载结果返回给Status参数同时改变NewHand的值。 使用EFI_ERROR()函数判断是否加载成功。 */StatusgBS-LoadImage(FALSE,*ParentImageHandle,(EFI_DEVICE_PATH_PROTOCOL*)DevicePath,NULL,0,NewHandle);if(EFI_ERROP(Status)){if(NewHandle!NULL)gBS-UnloadImage(NewHandle);//这里为什么要UnloadImagereturn(Status);}/* 第二步获取命令行参数并将获取的命令行参数交给UefiMain.efi的Image对象即句柄NewHandle gBS-OpenProtocol()函数传入所得的信息和NewHandle。 */StatusgBS-OpenProtocol(NewHandle,gEfiLoadedImageProtocolGuid,(VOID**)LoadedImage,gImageHandle,NULL,EFI_OPEN_PROTOCOL_GET_PROTOCOL);if(!EFI_ERROR(Status)){ASSERT(LoadedImage-LoadOptionsSize0;if(NULL!CommandLine){LoadedImage-LoadOptionsSize(UINT32)StrSize(CommandLine));LoadedImage-LoadOptions(VOID*)CommandLine;}}.../* 第三步启动加载的Image gBS-StartImage()函数根据所传入的NewHandle启动相应的Image。 */if(!EFI_ERROR(Status)){if(StatusCode!UNLL)*StatusCodegBS-StartImage(NewHandle,NULL,NULL);elseStatusgBS-StartImage(NewHandle,NULL,NULL);}...//退出应用程序后清理资源}加载应用程序中最重要的一步就是gBS-StartImage(NewHandle, NULL, NULL)程序通过这一步启动所加载的Image。StartImage的主要租用作用是找出可执行程序映像Image的入口函数并执行gBS-StartImage是函数指针实际指向CoreStartImage函数。进入映像函数的入口函数CoreStartImage的主要作用是调用映像的入口函数。具体代码清单//file MdeModulePkg\Core\Dxe\Image\Image.cEFI_STATUS EFIAPICoreStartImage(IN EFI_HANDLE ImageHandle,OUT NINTN*ExitDataSize,OUT CHAR16**ExitData OPTIONDAL){//定义参数EFI_STATUS Status;LOADED_IMAGE_PRIVATE_DATA*Image;LOADED_IMAGE_PRIVATE_DATA*LastImage;UINT64 HandleDatabaseKey;UINTN SetJumpFlag;UINT64 Tick;EFI_HANDLE Handle;//设置LongJump用于退出此程序Image-JumpBufferAllocatePool(sizeof(BASE_LIBRARY_JUMP_BUFFER)BASE_LIBRARY_JUMP_BUFFER_ALLGNMENT);if(Image-JumpBufferNULL)returnEFI_OUT_OF_RESOURCES;Image-JumpContextALIGN_POINTER(Image-JumpBuffer,BASE_LIBRARY_JUMP_BUFFER_ALIGNMENT);SetJumpFlageSetJump(Image-JumpContest);//首次调用SetJump()返回0。通过 LongJump(Image-JumpCOntext) 跳转到此处时返回非零值。if(0SetJumpFlag){//调用Image的入口函数Image-StartedTRUE;Image-StatusImage-ENtryPoint(ImageHandle,Image-Info.SystemTable);//设置Image执行后的状态然后通过LongJump跳到应用程序退出点。CoreExit(ImageHandle,Image-Status,0,NULL);}/* 此处是应用程序退出点。 程序通过LongJump跳转到此处然后根据Image-Status进行错误处理。 */...}在gBS-StartImage中SetJump/LongJump为应用程序的执行提供了一种错误处理机制执行流程如图所示gBS-StartImage的核心是Image-EntryPoint(···)它是程序映像Image的入口函数应用程序的入口函数是_ModuleEntryPoint。进入_ModuleEntryPoint后控制权才转交给应用程序此例的应用程序指UefiMain.efi_ModuleEntryPoint的代码如下//file MdePkg\UefiApplicationEntryPoint\ApplicationEntryPoint.cEFI_STATUS EFIAPI_ModuleEntryPoint(IN EFI_HANDLE ImageHandle,IN EFI _SYSTEM_TABLE*SystemTable){EFI_STATUS Status;if(0!_gUefiDriverRevision){//确保系统平台的UEFI版本号大于等于ImageHandle的UEFI版本号if(SystemTable-Hdr.Revision_gUefiDriverRevision)returnEFI_INCOMPATIBLE_VERSION;}//调用所有将被调用的库的构造函数进行初始化。ProcessLibraryConstructorList(ImageHandle,SystemTable);//调用Image的入口函数StatusProcessModuleEntryPointList(ImageHandle,SystemTable);//所有库的析构函数ProcessLibraryDestructorList(ImageHandle,SystemTable);returnStatus;}_ModuleEntryPoint主要处理一下三件事初始化在初始化函数ProcessLibraryConstructorList中会调用一系列构造函数。调用本模块的入口函数在ProcessModuleEntryPointList调用应用程序工程模块真正的入口函数即在 .inf 文件中定义的入口函数UefiMain。析构在析构函数ProcessLibraryDestructorList函数中会调用一系列的析构函数。这三个Process*函数是在系统执行build命令时build命令解析模块的工程文件即.inf文件然后生成AutoGen.h和AutoGen.c其中AutoGen.c中便含有这三个函数。一般而言在.inf文件的[LibraryClass]段声明了某个库后如果这个库由构造函数AutoGen便会在ProcessLibraryConstructorList中加入这个库的构造函数。另外ProcessLibraryConstructorList还会自动加入启动服务(BS)和运行时服务(RT)的构造函数。工程模块HelloWorld的ProcessLibraryConstructorList函数代码如下\VOID EFIAPIProcessLibraryConstructorList(IN EFI_HANDLE ImageHandle,IN EFI_SYSTEM_TABLE*SystemTable){EFI_STATUS Status;//初始化全局变量gBS、gST和gImageHandleStatusUefiBootServicesTableLibConstructor(ImageHanle,SystemTable);ASSERT_EFI_ERROR(Status);//初始化全局变量gRTStatusUefiRuntimeServicesTableLibConstructor(ImageHandle,SystemTable);ASSERT_EFI_ERROR(Status);//初始化UefiLibPrint函数是在UefiLib中实现的StatusUefiLibConstructor(ImageHandle,SystemTable);ASSERT_EFI_ERROR(Status);}gBS指向启动服务表gST指向系统表(System Table)指向正在执行的驱动或应用程序。gRT指向运行时服务表。这几个全局变量在开发应用程序和驱动时会经常用到。使用gBS、gST、gImageHandle前需要#includeinclude/UefiBootServocesTableLib.h。使用gRT之前需要加入#includeinclude/UefiRuntimeServicesTableLib.h。与构造函数相似AutoGen会在析构函数中调用相应Library的析构函数。HelloWorld标准应用程序工程模块的析构函数ProcessLibraryDestructorList为空这是因为UefiBootServicesTableLib、UefiRuntimeServicesTableLib、UefiLib这三个Library都没有析构函数。标准应用程序工程HelloWorld的析构函数ProcessLibraryDestructorList代码如下VOID EFIAPI ProcessLibraryDesturctorList (IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable) { }进入模块入口函数在ProcessModuleEntryPointList中调用了应用程序工程模块的真正入口函数UefiMain代码如下EFI_STATUS EFIAPI ProcessModuleEntryPointList(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable) { return UefiMain(ImageHandle, SystemTable); }总结标准应用工程模块入口函数的整个调用过程为​ -PLCL 运行所有构造函数LoadImage-StartImage-_ModuleEntryPoint-PMEP-UefiMain 执行入口函数-PLDL 运行所有析构函数5、总结本篇介绍了标准应用程序工程的模块的组成、入口函数UefiMain的结构以及.inf文件的结构
返回列表