UEFI编程入门-环境准备

环境准备

准备

需要安装Visual Studio 2019、Python 2.7+、IASL编译器、NASM编译器,并将后两者加入PATH。

其中IASL编译器下载地址https://www.intel.com/content/www/us/en/developer/topic-technology/open/acpica/download.html 。NASM编译器下载地址https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/win64/

下载EDK2开发包:

1
2
git clone https://github.com/tianocore/edk2.git
git clone https://github.com/tianocore/edk2-libc.git

EDK2中通过Submodule方式提供了些必要的库文件和编译所需源文件,这里对他们进行初始化和更新,在edk2目录下:

1
git submodule update --init #以后再更新就不用加--init了

再在edk2目录下编译BaseTools,这里要打开Visual Studio的x86 Native Tools Command Prompt。编译好的BaseTools在BaseTools目录中。

1
edksetup.bat Rebuild

接着为了方便后续添加新开发包,这里编写一个设置开发工具路径的批处理。在edk2和edk2-libc同目录下,新建mybuild.bat,添加:

1
2
3
4
set WORKSPACE=%CD%
set EDK_TOOLS_PATH=%CD%\edk2\BaseTools
set CONF_PATH=%CD%\edk2\conf
set PACKAGES_PATH=%CD%\edk2;%CD%\edk2-libc

UEFI编译依赖edk2/Conf目录下的target.txt和tools_def.txt。分别指定编译默认参数和编译工具链。例如target.txt的设置:

1
2
3
4
5
6
ACTIVE_PLATFORM       = EmulatorPkg/EmulatorPkg.dsc #目前正在编译的包
TARGET = DEBUG #编译目标类型 可以是DEBUG、RELEASE、NOOPT
TARGET_ARCH = IA32 #程序运行目标架构 可以是IA32、IPF、X64、EBC、ARM、AARCH64
TOOL_CHAIN_CONF = Conf/tools_def.txt #编译链工具配置文件位置
TOOL_CHAIN_TAG = VS2019 #编译工具链
BUILD_RULE_CONF = Conf/build_rule.txt #编译规则文件

在tools_def.txt中有大量可用编译器,这个就不需要管了。

编译

先编译UEFI模拟器EmulatorPkg,在x86 Native Tools Command Prompt中:

1
2
3
mybuild.bat
edk2\edksetup.bat
build -p edk2\EmulatorPkg\EmulatorPkg.dsc -t VS2022 -a IA32 #-p对应ACTIVE_PLATFORM -t对应TOOL_CHAIN_TAG 64位则换成-a X64 这里不指定则用target.txt中参数编译

再编译UEFI程序,同样在x86 Native Tools Command Prompt中:

1
2
3
4
5
6
7
8
9
#例如编译edk2\-libc中的AppPkg包:
mybuild.bat
edk2\edksetup.bat
build -p edk2-libc\AppPkg\AppPkg.dsc -t VS2022 -a IA32

#例如编译MdeModulePkg包中的HelloWorld:
mybuild.bat
edk2\edksetup.bat
build -p edk2\MdeModulePkg\MdeModulePkg.dsc -m edk2\MdeModulePkg\Application\HelloWorld\HelloWorld.inf -t VS2022 -a IA32

编译出来的目标程序都在Build目录下。

模拟

运行WinHost:

1
2
cd Build\EmulatorIA32\DEBUG_VS2022\IA32
WinHost.exe

成功启动后进入UEFI Shell,显示挂载了一些设备,其中FS0为主机Build\EmulatorIA32\DEBUG_VS2022\IA32目录。UEFI Shell命令与DOS或Bash差不多,如lsdirpci等,用help -b查看帮助。这里运行helloworld:

1
2
fs0:
HelloWorld.efi

调试

Visual Studio 2022联动

这里对HelloWorld.efi进行调试。在edk2\MdeModulePkg\Application\HelloWorld目录下用Visual Studio 2022新建一个生成文件项目(Makefile Project),工程名随便起,这里叫x43dbg_vs,其中生成命令行、清除命令行和重新生成命令行先随便填,接下来要改,其他不用管。之后打开x32dbg_vs.vcxproj并修改对应DEBUG选项的NMakeBuildCommandLine标签内容:

1
2
3
4
5
cd /D D:\tests\UEFItest
set WORKSPACE=D:\tests\UEFItest
call mybuild.bat
call edk2\edksetup.bat
call build.bat -p EmulatorPkg\EmulatorPkg.dsc -a IA32 -m MdeModulePkg\Application\HelloWorld\HelloWorld.inf -b DEBUG

修改NMakeCleanCommandLine标签内容:

1
2
3
4
5
cd /D D:\tests\UEFItest
set WORKSPACE=D:\tests\UEFItest
call mybuild.bat
call edk2\edksetup.bat
call build.bat -p EmulatorPkg\EmulatorPkg.dsc -a IA32 -m MdeModulePkg\Application\HelloWorld\HelloWorld.inf -b DEBUG clean

修改NMakeReBuildCommandLine标签内容:

1
2
3
4
5
6
cd /D D:\tests\UEFItest
set WORKSPACE=D:\tests\UEFItest
call mybuild.bat
call edk2\edksetup.bat
call build.bat -p EmulatorPkg\EmulatorPkg.dsc -a IA32 -m MdeModulePkg\Application\HelloWorld\HelloWorld.inf -b DEBUG clean
call build.bat -p EmulatorPkg\EmulatorPkg.dsc -a IA32 -m MdeModulePkg\Application\HelloWorld\HelloWorld.inf -b DEBUG

再手动把edk2\MdeModulePkg\Application\HelloWorld\HelloWorld.c导入源文件下,之后进行Build,发现下方输出窗口内容同上述手动编译过程。然后在项目属性中设置调试->工作目录为D:\tests\UEFItest\Build\EmulatorIA32\DEBUG_VS2022\IA32\,命令改为WinHost.exe。之后对源码打断点,并用F5启动调试。随即启动WinHost.exe,在其中运行HelloWorld.efi,发现在Visual Studio中命中断点。

QEMU+WinDBG联动

先下载QEMU和Intel UDK Debugger Tool,前者在https://qemu.weilnetz.de/w64/ ,后者在https://www.intel.com/content/www/us/en/developer/articles/tool/unified-extensible-firmware-interface.html 。Intel UDK Debugger Tool安装时选择Pipe类型,Port填qemu_pipe_dbg。WinDBG和Intel UDK Debugger Tool安装目录上不要有汉字。

接下来为QEMU创建VHD虚拟硬盘。打开Windows自带磁盘管理工具,选中某个磁盘创建VHD,200MB大小即可,命名为lbdbg.vhd,并设为动态增长模式。然后对刚生成的虚拟硬盘文件装载,格式化为FAT32分区,再弹出。

接着用OvmfPkg制作一个支持源码级调试的BIOS镜像,例如32位:

1
build -a IA32 -p edk2\OvmfPkg\OvmfPkgIa32.dsc -b NOOPT -D SOURCE_DEBUG_ENABLE #X64改为-a X64 OvmfPkgX64.dsc

编译出的文件位于Build\OvmfIa32\NOOPT_VS2022\FV目录下的OVMF.fd,并将HelloWorld.efi复制到lbdbg.vhd分区中,弹出。找个地方新建dbgOvmfIa32文件夹,将lbdbg.vhd和OVMF.fd复制到这里。打开开始菜单的Start Windbg With Intel UDK Debugger Tool,如果弹出不支持该内核调试方法,则自行启动管道调试。在dbgOvmfIa32目录打开命令行,运行:

1
qemu-system-i386 -L . -bios OVMF.fd -hdd lbdbg.vhd -serial pipe:qemu_pipe_dbg

运行后用下面命令下断点,并用g继续:

1
bu HelloWorld!UefiMain

制作启动盘

这里只讲针对UEFI BIOS制作的启动盘。启动文件通过编译ShellPkg得到:

1
2
3
mybuild.bat
edk2\edksetup.bat
build -a IA32 -a X64 -p ShellPkg\ShellPkg.dsc -t VS2022 -b RELEASE

在Build\Shell\RELEASE_VS2022下的IA32和X64目录下各有一个Shell.efi,所处目录类似于Build\Shell\RELEASE_VS2022\IA32\ShellPkg\Application\Shell\EA4BB293-2D7F-4456-A681-1F22F42CD0BC\OUTPUT,分别改名为bootx32.efi和bootx64.efi。之后将U盘格式化为FAT32格式,在U盘根目录下建立efi\boot目录,将bootx32.efi和bootx64.efi复制到该目录下。重启计算机,开机从U盘启动即可进入UEFI Shell。

应用构建

配置文件

代码中一定不要有中文,注释也不行,否则编译出问题。

HelloWorld的INF配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[Defines] #变量定义
INF_VERSION = 0x00010005 #INF版本号
BASE_NAME = HelloWorld #编译输出文件名
MODULE_UNI_FILE = HelloWorld.uni #UCS-2型字符串 文件地址相对于INF所在目录
FILE_GUID = 6987936E-ED34-44db-AE97-1FA5E4ED2116 #工程文件的GUID
MODULE_TYPE = UEFI_APPLICATION #模块类型
VERSION_STRING = 1.0 #模块版本字符串
ENTRY_POINT = UefiMain #模块入口函数
UEFI_HII_RESOURCE_SECTION = TRUE #使用HII资源
[Sources] #源代码和资源文件
HelloWorld.c
HelloWorldStr.uni
[Packages] #DEC包声明文件
MdePkg/MdePkg.dec
MdeModulePkg/MdeModulePkg.dec
[LibraryClasses] #库
UefiApplicationEntryPoint
UefiLib
PcdLib
[FeaturePcd] #一般不用
gEfiMdeModulePkgTokenSpaceGuid.PcdHelloWorldPrintEnable ## CONSUMES
[Pcd] #PCD变量 一般不用
gEfiMdeModulePkgTokenSpaceGuid.PcdHelloWorldPrintString ## SOMETIMES_CONSUMES
gEfiMdeModulePkgTokenSpaceGuid.PcdHelloWorldPrintTimes ## SOMETIMES_CONSUMES
[UserExtensions.TianoCore."ExtraFiles"] #自定义节 一般不用
HelloWorldExtra.uni

每次新建工程时,在edk2-lib\AppPkg\Application\目录下新建文件夹,文件夹中需要有一个.c源代码和一个.inf文件,然后打开对应包下的DSC文件,在Components节中加入:

1
2
[Components]
AppPkg/Applications/程序名/源代码名.inf

入口函数

UEFI应用的入口函数有三种,功能不同。对于UefiMain型的:

1
2
3
4
5
6
7
8
9
10
11
#include <Uefi.h>
#include <Library/UefiLib.h>
EFI_STATUS EFIAPI UefiMain(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE* SystemTable) {
EFI_TIME curTime;
Print(L"Hello,this is Entry of UefiMain!\n");
SystemTable->BootServices->Stall(2000); //延时2s
SystemTable->RuntimeServices->GetTime(&curTime, NULL);
Print(L"Current Time: %d-%d-%d %02d:%02d:%02d\n", curTime.Year, curTime.Month, curTime.Day, curTime.Hour, curTime.Minute, curTime.Second);
SystemTable->ConOut->OutputString(SystemTable->ConOut, L"Test SystemTable...\n\r");
return EFI_SUCCESS;
};

ImageHandle指向模块自身加载到内存的Image对象句柄,通过SystemTable可获得UEFI提供的各种服务。

UEFI Images即UEFI镜像,是UEFI规范的包含可执行代码的二进制程序文件,分为UEFI应用程序和UEFI驱动,采用PE32文件结构。UEFI Services即UEFI服务,是平台调用接口集合,允许UEFI程序和操作系统调用,这些接口由UEFI应用程序、驱动和UEFI OS Loader提供,涉及运行时服务RS和启动服务BS。

UefiMain型INF文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = Uefi_Main
FILE_GUID = 6937936E-ED34-44ab-AE97-1FA5E7ED2116
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 1.0
ENTRY_POINT = UefiMain
[Sources]
Uefi_Main.c
[Packages]
MdePkg/MdePkg.dec
MdeModulePkg/MdeModulePkg.dec
[LibraryClasses]
UefiApplicationEntryPoint
UefiLib

常用的头文件有:

库函数 内容
Uefi.h 基本数据类型和核心数据结构
Library/UefiLib.h 通用库函数,时间、简单锁、任务优先级、驱动管理、字符图形显示输出等
Library/BaseLib.h 字符串处理、数学、文件路径处理等
Library/BaseMemoryLib.h 内存库函数,内存拷贝、填充、清空等
Library/DebugLib.h 调试输出库函数

SystemTable结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct {
EFI_TABLE_HEADER Hdr;
CHAR16 *FirmwareVendor;
UINT32 FirmwareRevision;
EFI_HANDLE ConsoleInHandle;
EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn; //设备输入指针 一般键盘
EFI_HANDLE ConsoleOutHandle;
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut; //设备输出指针
EFI_HANDLE StandardErrorHandle;
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *StdErr;
EFI_RUNTIME_SERVICES *RuntimeServices; //RS服务入口指针
EFI_BOOT_SERVICES *BootServices; //BS服务入口指针
UINTN NumberOfTableEntries;
EFI_CONFIGURATION_TABLE *ConfigurationTable;
} EFI_SYSTEM_TABLE;

对于ShellAppMain型的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/ShellCEntryLib.h>
#include <Library/UefiBootServicesTableLib.h> //gST,gBs
#include <Library/UefiRuntimeServicesTableLib.h> //gRT
INTN EFIAPI ShellAppMain(IN UINTN Argc, IN CHAR16** Argv) {
EFI_TIME curTime;
Print(L"Hello,this is Entry of ShellAppMain!\n");
gBS->Stall(2000);
gRT->GetTime(&curTime, NULL);
Print(L"Current Time: %d-%d-%d %02d:%02d:%02d\n", curTime.Year, curTime.Month, curTime.Day, curTime.Hour, curTime.Minute, curTime.Second);
gST->ConOut->OutputString(gST->ConOut, L"Test SystemTable...\n\r");
return(0);
}

相比UefiMain型,ShellAppMain型能接收命令行参数。该型INF文件格式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Defines]
INF_VERSION = 0x00010006
BASE_NAME = ShellApp_Main
FILE_GUID = a912f198-7f0e-4813-b918-b757b106ec83
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 0.1
ENTRY_POINT = ShellCEntryLib
[Sources]
ShellApp_Main.c
[Packages]
MdePkg/MdePkg.dec
ShellPkg/ShellPkg.dec
[LibraryClasses]
UefiLib
ShellCEntryLib

EDK2-LIBC工程允许UEFI程序直接使用C标准库,并使用main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include  <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/ShellCEntryLib.h>
#include <Library/UefiBootServicesTableLib.h> //gST,gBs
#include <Library/UefiRuntimeServicesTableLib.h> //gRT
#include <stdio.h>
#include <stdlib.h>
int main(IN int Argc, IN char** Argv) {
EFI_TIME curTime;
printf("Hello,this is Entry of main!\n"); //直接用printf
gBS->Stall(2000);
gRT->GetTime(&curTime, NULL);
printf("Current Time: %d-%d-%d %02d:%02d:%02d\n", curTime.Year, curTime.Month, curTime.Day, curTime.Hour, curTime.Minute, curTime.Second);
gST->ConOut->OutputString(gST->ConOut, L"Test SystemTable...\n\r");
return 0;
};

Stdlib型INF文件格式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[Defines]
INF_VERSION = 0x00010006
BASE_NAME = Stdlib_Main
FILE_GUID = 4ea97c46-1491-4dfd-b412-747010f31e5f
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 0.1
ENTRY_POINT = ShellCEntryLib
[Sources]
Stdlib_Main.c
[Packages]
StdLib/StdLib.dec
MdePkg/MdePkg.dec
ShellPkg/ShellPkg.dec
[LibraryClasses]
LibC
LibStdio

库模块

对于库模块的编写,INF文件格式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = MyLibraryLib
FILE_GUID = 6937936E-ED24-443b-AEe7-1FA5E7ED21A6
MODULE_TYPE = BASE #修改
VERSION_STRING = 1.0
LIBRARY_CLASS = MyLibraryLib #库文件名
CONSTRUCTOR = MyLibConstructor #初始化函数
DESTRUCTOR = MyLibDestructor #回收资源函数
[Sources]
MyLibrary.c
MyLibrary.h
[Packages]
MdePkg/MdePkg.dec
MdeModulePkg/MdeModulePkg.dec
[LibraryClasses]
UefiLib

库源码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//MyLibrary.h
#ifndef _MYLIBRARY_H
#define _MYLIBRARY_H
#include <Uefi.h>
VOID LibFunction(VOID);
#endif

//MyLibrary.c
#include <Uefi.h>
#include <Library/UefiLib.h>
VOID LibFunction(VOID){
Print(L"LibFunction() is called!\n"); //2
};
RETURN_STATUS EFIAPI MyLibConstructor(VOID ){
Print(L"MyLibConstructor() is called!\n"); //1
};
RETURN_STATUS EFIAPI MyLibDestructor (VOID){
Print(L"MyLibConstructor() is called!\n"); //3
};

对于应用本库的工程,INF文件格式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Defines]
INF_VERSION = 0x00010006
BASE_NAME = MyLibApp
FILE_GUID = 4ea92c46-1491-4dfd-c412-747013f31e5f
MODULE_TYPE = UEFI_APPLICATION
VERSION_STRING = 0.1
ENTRY_POINT = ShellCEntryLib
[Sources]
MyLibApp.c
[Packages]
StdLib/StdLib.dec
MdePkg/MdePkg.dec
ShellPkg/ShellPkg.dec
[LibraryClasses]
LibC
LibStdio
MyLibraryLib #加上这个

该应用源码为:

1
2
3
4
5
6
7
8
#include  <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/ShellCEntryLib.h>
#include <../MyLibrary/MyLibrary.h>
int main(IN int Argc,IN char **Argv){
LibFunction();
return 0;
};

然后AppPkg包的DSC文件还要添加:

1
2
3
4
5
[Components]
AppPkg/Applications/LibSample/MyLibApp/MyLibApp.inf{
<LibraryClasses>
MyLibrary|AppPkg/Applications/LibSample/MyLibrary/MyLibrary.inf
}

其他工程文件

DEC文件配合DSC文件,描述包的公开数据和接口。IDF、UNI和VFR为资源文件,提供图像、文字和框架资源。DSC、DEC和INF文件配合源代码和资源文件,用Build工具生成.efi、.lib等二进制文件。FDF文件用于生成Option ROM镜像、固件镜像和可启动镜像,与DSC文件和二进制文件用GenFw工具生成镜像,如OVMF镜像。

UEFI Protocol

可通过启动服务的HandleProtocolOpenProtocol找到设备句柄,句柄与Protocol接口联系在一起,可通过Protocol接口的函数指针访问Protocol接口函数。每个Protocol都有GUID、Protocol接口结构体和Protocol服务。下面是个例子:

1
2
3
4
5
6
7
8
9
10
11
12
#define EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID{0x9042a9de, 0x23dc, 0x4a38, {0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a}} //该Protocol的GUID
typedef struct _EFI_GRAPHICS_OUTPUT_PROTOCOL EFI_GRAPHICS_OUTPUT_PROTOCOL;
struct _EFI_GRAPHICS_OUTPUT_PROTOCOL { //这4个成员全是函数指针
EFI_GRAPHICS_OUTPUT_PROTOCOL_QUERY_MODE QueryMode;
EFI_GRAPHICS_OUTPUT_PROTOCOL_SET_MODE SetMode; //回调原型如下
EFI_GRAPHICS_OUTPUT_PROTOCOL_BLT Blt;
EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode;
};
typedef EFI_STATUS(EFIAPI *EFI_GRAPHICS_OUTPUT_PROTOCOL_SET_MODE)(
IN EFI_GRAPHICS_OUTPUT_PROTOCOL *This, //指向Protocol指针 用于区分Protocol实例
IN UINT32 ModeNumber //需设置的显示模式
);

使用Protocol步骤如下:

  • 在UEFI规范中找到需要用的Protocol的GUID,用OpenProtocolHandleProtocolLocateProtocol找出Protocol实例,若多个设备支持此Protocol,则要用LocateHandleBufferLocateHandle列出所有句柄,再用OpenProtocol等找出Protocol实例。
  • 用该Protocol提供的接口函数来实现所需功能。
  • CloseProtocol关闭打开的Protocol实例。

OpenProtocol如下,对于给定句柄查询是否支持指定Protocol,支持则打开对应Protocol实例,在入口参数中保存对象指针并返回EFI_SUCCESS,不支持则返回相应错误代码。若Handle的Protocol链表中有该Protocol,则Protocol实例指针将写到*Interface中。UEFI驱动和应用中的该函数参数含义有所不同。UEFI驱动下ControllerHandle是拥有该驱动的控制器,AgentHandle是拥有该EFI_DRIVER_BINDING_PROTOCOL实例的句柄。UEFI应用下ControllerHandle可忽略,AgentHandle是该程序句柄,即UefiMain函数第一个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef EFI_STATUS (EFIAPI *EFI_OPEN_PROTOCOL)(
IN EFI_HANDLE Handle, //打开此Handle安装的Protocol接口
IN EFI_GUID *Protocol, //指向要打开的Protocol的GUID
OUT VOID **Interface OPTIONAL, //返回打开的Protocol实例 没有则NULL
IN EFI_HANDLE AgentHandle, //打开此Protocol的Image
IN EFI_HANDLE ControllerHandle OPTIONAL, //控制Protocol接口的控制器
IN UINT32 Attributes //打开Protocol的参数 取值如下
);
#define EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL 0x00000001 //类似HandleProtocol
#define EFI_OPEN_PROTOCOL_GET_PROTOCOL 0x00000002 //UEFI驱动用该参数获取相应Protocol
#define EFI_OPEN_PROTOCOL_TEST_PROTOCOL 0x00000004 //UEFI驱动测试句柄上是否有相应Protocol
#define EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER 0x00000008 //总线驱动用 决定子控制器是否使用相应Protocol
#define EFI_OPEN_PROTOCOL_BY_DRIVER 0x00000010 //UEFI驱动获得Protocol使用权 其他驱动无法再获得
#define EFI_OPEN_PROTOCOL_EXCLUSIVE 0x00000020 //UEFI应用获得Protocol独占使用权

例如打开XyzIo:

1
2
3
4
5
6
7
EFI_BOOT_SERVICES* gBS;
EFI_HANDLE ImageHandle;
IN EFI_HANDLE ControllerHandle;
extern EFI_GUID gEfiXyzIoProtocol;
EFI_XYZ_IO_PROTOCOL* XyzIo;
EFI_STATUS Status;
Status = gBS->OpenProtocol(ControllerHandle, &gEfiXyzIoProtocol, &XyzIo, ImageHandle, NULL, EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);

上面gEfiXyzIoProtocol是EFI_XYZ_IO_PROTOCOL型Protocol相关的GUID,值一般定义在DEC文件中,UEFI应用或驱动应在INF文件中声明此GUID。

HandleProtocolOpenProtocol的简化版本:

1
2
3
4
5
6
7
8
9
typedef EFI_STATUS (EFIAPI *EFI_HANDLE_PROTOCOL)(
IN EFI_HANDLE Handle,
IN EFI_GUID *Protocol,
OUT VOID **Interface
);
//EFI_SUCCESS 执行成功 返回Protocol实例 存在参数Interface中
//EFI_UNSUPPORTED 失败 设备不支持指定Protocol
//EFI_INVALID_PARAMETER 失败 无效参数输入
//原型:OpenProtocol(Handle,Protocol,Interface,EfiCoreImageHandle,NULL,EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);

LocateProtocol不用设备句柄,自己在系统中找句柄列表,返回找到的第一个:

1
2
3
4
5
6
7
typedef EFI_STATUS(EFIAPI *EFI_LOCATE_PROTOCOL)(
IN EFI_GUID *Protocol,
IN VOID *Registration OPTIONAL, //从BS的RegisterProtocolNotify获得的注册key 一般NULL
OUT VOID **Interface
);
//EFI_SUCCESS EFI_INVALID_PARAMETER
//EFI_NOT_FOUND 无法找到匹配Protocol和Registration的Protocol

一般用法如:

1
2
EFI_GRAPHICS_OUTPUT_PROTOCOL* gGraphicsOutput;
gBS->LocateProtocol(&gEfiGraphicsOutputProtocolGuid, NULL, &gGraphicsOutput);

LocateHandleLocateHandleBuffer通过GUID找设备句柄。前者缓冲区需要调用者准备,后者内部实现,但两者内存释放都要调用者完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef EFI_STATUS(EFIAPI *EFI_LOCATE_HANDLE)(
IN EFI_LOCATE_SEARCH_TYPE SearchType, //查找方式
IN EFI_GUID *Protocol OPTIONAL, //待查找Protocol
IN VOID *SearchKey OPTIONAL, //一般NULL
IN OUT UINTN *BufferSize, //输入时为Buffer字节长度 输出时为Buffer所含数组长度
OUT EFI_HANDLE *Buffer //句柄数组缓冲区
);
//EFI_SUCCESS EFI_NOT_FOUND EFI_INVALID_PARAMETER
//EFI_BUFFER_SMALL BufferSize太小
typedef EFI_STATUS (EFIAPI *EFI_LOCATE_HANDLE_BUFFER)(
IN EFI_LOCATE_SEARCH_TYPE SearchType,
IN EFI_GUID *Protocol OPTIONAL,
IN VOID *SearchKey OPTIONAL,
OUT UINTN *NoHandles, //返回句柄数
OUT EFI_HANDLE **Buffer
);
//EFI_SUCCESS EFI_NOT_FOUND EFI_INVALID_PARAMETER
//EFI_OUT_OF_RESOURCE 系统无足够内容可分配以存储句柄数组
typedef enum {
AllHandles, //找出系统所有句柄
ByRegisterNotify, //找出匹配SearchKey的句柄 忽略Protocol参数
ByProtocol //找出所有支持Protocol GUID的设备句柄 忽略SearchKey
} EFI_LOCATE_SEARCH_TYPE;

例如找出所有串口Protocol实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//LoateHandle法
EFI_STATUS Status;
UINTN BufferSize = 0;
UINTN Index;
EFI_HANDLE* HandleBuffer = NULL;
EFI_SERIAL_IO_PROTOCOL* gSerialIoArray[256];
Status = gBS->LocateHandle(ByProtocol, &gEfiSerialIoProtocolGuid, NULL, &BufferSize, HandleBuffer); //获取实际需要的缓存大小
if (Status == EFI_BUFFER_TOO_SMALL) {
HandleBuffer = AllocateZeroPool(BufferSize);
if (HandleBuffer == NULL)
return EFI_OUT_OF_RESOURCES;
Status = gBS->LocateHandle(ByProtocol, &gEfiSerialIoProtocolGuid, NULL, &BufferSize, HandleBuffer); //获取所有支持串口GUID的句柄
for (Index = 0; Index < BufferSize / sizeof(EFI_HANDLE); Index++)
Status = gBS->HandleProtocol(HandleBuffer[Index], &gEfiSerialIoProtocolGuid, (VOID**)&(gSerialIoArray[Index])); //通过句柄找Protocol实例
if (HandleBuffer != NULL)
Status = gBS->FreePool(HandleBuffer);
};

//LocateHandleBuffer
EFI_STATUS Status;
EFI_HANDLE* SerialHandleBuffer = NULL;
UINTN HandleIndex = 0;
UINTN HandleCount = 0;
EFI_SERIAL_IO_PROTOCOL* gSerialIOArray[256];
Status = gBS->LocateHandleBuffer(ByProtocol, &gEfiSerialIoProtocolGuid, NULL, &HandleCount, &SerialHandleBuffer);
if (EFI_ERROR(Status))
return Status;
nSerialIO = HandleCount;
for (HandleIndex = 0; HandleIndex < HandleCount; HandleIndex++) {
Status = gBS->HandleProtocol(SerialHandleBuffer[HandleIndex], &gEfiSerialIoProtocolGuid, (VOID**)&(gSerialIOArray[HandleIndex]));
if (EFI_ERROR(Status))
continue;
};
if (SerialHandleBuffer != NULL)
Status = gBS->FreePool(SerialHandleBuffer);

OpenProtocolInformatin获得Protocol实例详细信息。每次Protocol被打开,都添加一项该结构,若有相同项则直接OpenCount加一。关闭时减一直至删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef EFI_STATUS(EFIAPI *EFI_OPEN_PROTOCOL_INFORMATION)(
IN EFI_HANDLE Handle, //设备句柄
IN EFI_GUID *Protocol, //待查询Protocol GUID
OUT EFI_OPEN_PROTOCOL_INFORMATION_ENTRY **EntryBuffer, //Protocol信息 调用者释放
OUT UINTN *EntryCount //指向EntryBuffer
);
//EFI_SUCCESS EFI_NOT_FOUND EFI_OUT_OF_RESOURCE
typedef struct {
EFI_HANDLE AgentHandle; //使用者句柄
EFI_HANDLE ControllerHandle; //控制器句柄
UINT32 Attributes; //属性
UINT32 OpenCount; //打开的数目
} EFI_OPEN_PROTOCOL_INFORMATION_ENTRY;

用完Protocol后用CloseProtocol关闭:

1
2
3
4
5
6
7
typedef EFI_STATUS(EFIAPI *EFI_CLOSE_PROTOCOL)(
IN EFI_HANDLE Handle, //设备句柄
IN EFI_GUID *Protocol, //指定Protocol GUID
IN EFI_HANDLE AgentHandle, //使用者句柄
IN EFI_HANDLE ControllerHandle //控制器句柄
);
//EFI_SUCCESS EFI_INVALID_PARAMETER EFI_NOT_FOUND

例如Protocol常用的使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include  <Uefi.h>
#include <Library/UefiLib.h>
#include <Protocol/SerialIo.h> //串口?
// #include <Library/ShellCEntryLib.h>
#include <Library/DebugLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/BaseLib.h>
#include <Protocol/PciIo.h> //获取PciIO protocol所需
#include <Protocol/PciRootBridgeIo.h> //获取PciRootBridgeIO protocol所需
#include <IndustryStandard/Pci.h> //pci访问所需的头文件,包含pci22.h,pci23.h...
#include <Protocol/Rng.h> //Random Number Generator Protocol 2019-08-31 11:32:03 robin
#include <Protocol/SimpleFileSystem.h> //文件系统访问
#include <IndustryStandard/Bmp.h> //for bmp
EFI_STATUS ListProtocolMsg(IN EFI_GUID* ProtocolGuid, OUT VOID** Interface);
EFI_STATUS EFIAPI UefiMain(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE* SystemTable) {
EFI_RNG_PROTOCOL* gRNGOut; //测试随机数Protocol
EFI_SERIAL_IO_PROTOCOL* gSerialIO; //测试串口Protocol
gST->ConOut->SetAttribute(gST->ConOut, EFI_BACKGROUND_BLACK | EFI_RED); //黑底红字
Print((const CHAR16*)L"Action: SerialIoProtocol\n");
ListProtocolMsg(&gEfiSerialIoProtocolGuid, (VOID**)&gSerialIO); //串口Protocol
gST->ConOut->SetAttribute(gST->ConOut, EFI_BACKGROUND_BLACK | EFI_CYAN); //黑底青字
Print((const CHAR16*)L"Action: RngProtocol\n");
ListProtocolMsg(&gEfiRngProtocolGuid, (VOID**)&gRNGOut); //随机数Protocol
gST->ConOut->SetAttribute(gST->ConOut, EFI_BACKGROUND_BLACK | EFI_LIGHTGRAY);
return EFI_SUCCESS;
}
EFI_STATUS ListProtocolMsg(IN EFI_GUID* ProtocolGuid, OUT VOID** Interface) {
EFI_STATUS Status;
EFI_HANDLE* myHandleBuff = NULL;
// UINTN HandleIndex = 0;
UINTN HandleCount = 0;
UINTN i;
EFI_OPEN_PROTOCOL_INFORMATION_ENTRY* InfEntryArray;
UINTN InfEntryCount;
Print((const CHAR16*)L" GUID: {0x%08x, 0x%04x, 0x%04x, {", ProtocolGuid->Data1, ProtocolGuid->Data2, ProtocolGuid->Data3);
for (i = 0; i < 8; i++)
Print((const CHAR16*)L" 0x%02x", ProtocolGuid->Data4[i]);
Print((const CHAR16*)L"}}\n");
//1 通过Protocol GUID获取到设备句柄
Status = gBS->LocateHandleBuffer(ByProtocol, ProtocolGuid, NULL, &HandleCount, &myHandleBuff);//内存由函数分配,调用者记得释放
if (EFI_ERROR(Status)) {
Print((const CHAR16*)L"Not Found Handle!\n");
return Status;
}
else
Print((const CHAR16*)L"Found Handle Count: %d\n", HandleCount);
//2 顺序找到第一个Protocol
for (i = 0; i < HandleCount; i++) {
Status = gBS->HandleProtocol(myHandleBuff[i], ProtocolGuid, Interface);
if (EFI_ERROR(Status))
continue;
else
break;
}
//3 打印Protocol的信息
Status = gBS->OpenProtocolInformation(myHandleBuff[i], ProtocolGuid, &InfEntryArray, &InfEntryCount);
if (EFI_ERROR(Status))
Print((const CHAR16*)L"Not Get the Protocol's information!\n");
else {
Print((const CHAR16*)L"EntryCount=%d \n", InfEntryCount);
//4 关闭Protocol
gBS->CloseProtocol(myHandleBuff[i], ProtocolGuid, InfEntryArray->AgentHandle, InfEntryArray->ControllerHandle);
if (InfEntryArray) //释放被调函数申请的内存
gBS->FreePool(InfEntryArray);
}
//释放被调函数申请的内存
if (myHandleBuff)
gBS->FreePool(myHandleBuff);
return Status;
}