4、UEFI应用程序框架与入口点:编写UEFI Application的入口函数(ShellApp、Main)、参数解析、内存分配与释放、退出服务调用
UEFI应用程序(UEFI Application)是运行在UEFI固件环境中的可执行程序,与传统的操作系统应用程序不同,它直接运行在UEFI运行时环境中,没有操作系统内核的支持。理解UEFI应用程序的框架与入口点是开发UEFI工具、诊断程序、引导加载程序的基础。本章将深入剖析UEFI Application的入口函数结构、参数解析机制、内存管理接口以及退出服务的正确调用方式,帮助开发者构建健壮且符合规范的UEFI应用程序。
上图展示了UEFI Application从入口函数到退出服务的完整执行链路。入口函数接收系统表指针,经过参数解析、内存管理、业务逻辑处理后,最终通过退出服务返回状态码。下面我们将逐一深入每个环节。
4.1 入口函数框架:ShellAppMain与Main
核心原理:UEFI应用程序的入口点并非传统C语言的main()函数,而是由UEFI规范定义的EFI_IMAGE_ENTRY_POINT类型函数。对于Shell下的应用程序,入口函数通常命名为ShellAppMain;对于标准UEFI应用程序,则使用Main或UefiMain。两者的本质区别在于参数传递方式:ShellAppMain接收Shell参数结构体,而标准入口接收EFI_HANDLE和EFI_SYSTEM_TABLE指针。
入口函数签名:
// 标准UEFI Application入口
EFI_STATUS EFIAPI UefiMain(
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
);
// Shell下应用程序入口
EFI_STATUS EFIAPI ShellAppMain(
IN UINTN Argc,
IN CHAR16 **Argv
);
在EDK2框架中,UefiMain是默认的入口点,而ShellAppMain是Shell扩展包(ShellPkg)提供的封装。开发者可以通过INF文件中的ENTRY_POINT指令指定入口函数名称。
| 入口类型 | 参数 | 适用场景 | 获取系统表方式 |
|---|---|---|---|
| UefiMain | ImageHandle, SystemTable | 标准UEFI应用、驱动 | 直接使用SystemTable |
| ShellAppMain | Argc, Argv | Shell命令行工具 | 通过Shell协议间接获取 |
| ShellAppMain(扩展) | Argc, Argv, ShellParameters | 需要Shell环境交互 | ShellParameters->SystemTable |
UefiMain和ShellAppMain,链接器会优先使用UefiMain。建议在INF文件中明确指定ENTRY_POINT = ShellAppMain,避免歧义。
案例拆解:假设我们要开发一个显示系统内存信息的Shell工具。入口函数需要获取系统表,读取内存描述符。在ShellAppMain中,我们无法直接获得SystemTable,必须通过Shell协议或全局变量gST(由UefiLib自动初始化)来访问。
EFI_STATUS EFIAPI ShellAppMain(IN UINTN Argc, IN CHAR16 **Argv)
{
EFI_STATUS Status;
EFI_MEMORY_DESCRIPTOR *MemoryMap = NULL;
UINTN MapSize = 0, MapKey = 0, DescriptorSize = 0;
UINT32 DescriptorVersion = 0;
// 通过全局系统表指针访问Boot Services
Status = gBS->GetMemoryMap(&MapSize, MemoryMap, &MapKey,
&DescriptorSize, &DescriptorVersion);
if (Status == EFI_BUFFER_TOO_SMALL) {
Status = gBS->AllocatePool(EfiBootServicesData, MapSize, (VOID**)&MemoryMap);
if (EFI_ERROR(Status)) return Status;
Status = gBS->GetMemoryMap(&MapSize, MemoryMap, &MapKey,
&DescriptorSize, &DescriptorVersion);
}
// ... 处理内存信息
gBS->FreePool(MemoryMap);
return EFI_SUCCESS;
}
常见误区:初学者常将ShellAppMain与main()混淆,试图使用printf()等标准C库函数。UEFI环境没有标准C库,必须使用Print()或gST->ConOut->OutputString()进行输出。另外,入口函数必须使用EFIAPI调用约定(即Microsoft x64调用约定),否则会导致参数传递错误。
4.2 参数解析:从命令行到程序内部
核心原理:UEFI Shell应用程序通过Argc和Argv接收命令行参数。与C语言类似,Argv[0]是程序名称,Argv[1]到Argv[Argc-1]是用户传入的参数。Shell环境会自动处理引号、转义字符和通配符展开。
参数解析流程:
- 遍历
Argv数组,识别选项(以-或/开头)和位置参数。 - 对于选项参数,判断是否需要附加参数值(如
-s 1024)。 - 使用
StrCmp()、StrnCmp()等函数进行字符串比较。 - 将字符串参数转换为数值类型时,使用
StrToInt()或StrToInt64()。
// 参数解析示例
UINTN Index;
BOOLEAN Verbose = FALSE;
UINTN BlockSize = 512;
for (Index = 1; Index < Argc; Index++) {
if (StrCmp(Argv[Index], L"-v") == 0) {
Verbose = TRUE;
} else if (StrCmp(Argv[Index], L"-b") == 0) {
if (Index + 1 < Argc) {
BlockSize = StrToInt(Argv[++Index]);
if (BlockSize == 0) {
Print(L"Invalid block size!\n");
return EFI_INVALID_PARAMETER;
}
}
} else {
Print(L"Unknown option: %s\n", Argv[Index]);
return EFI_INVALID_PARAMETER;
}
}
高级参数处理:对于复杂的命令行工具,建议使用ShellPkg提供的ShellCommandLineParse()函数,它支持标志位、带值参数、位置参数和帮助信息自动生成。
*、?)会在传递给应用程序之前被Shell展开。如果不需要通配符展开,可以使用单引号包裹参数。
常见误区:不要直接修改Argv中的字符串,它们是只读的。如果需要修改,应先复制到本地缓冲区。另外,Argv中的字符串是UTF-16编码(CHAR16数组),不要与ASCII字符串直接比较。
4.3 内存分配与释放:Boot Services内存管理
核心原理:UEFI应用程序运行在Boot Services阶段,可以使用gBS->AllocatePool()和gBS->FreePool()进行动态内存管理。这些接口从系统内存池中分配内存,分配的内存类型决定了其用途和生命周期。
| 内存类型 | 含义 | 生命周期 | 典型用途 |
|---|---|---|---|
| EfiBootServicesData | 引导服务数据 | ExitBootServices后释放 | 临时缓冲区、动态数据结构 |
| EfiRuntimeServicesData | 运行时服务数据 | 操作系统运行时保留 | 需要传递给OS的数据 |
| EfiLoaderData | 加载器数据 | OS加载器使用 | 加载OS内核时的临时数据 |
| EfiACPIReclaimMemory | ACPI回收内存 | OS可回收 | ACPI表 |
内存分配接口:
EFI_STATUS AllocatePool(
IN EFI_MEMORY_TYPE PoolType, // 内存类型
IN UINTN Size, // 字节数
OUT VOID **Buffer // 返回分配的内存地址
);
EFI_STATUS FreePool(
IN VOID *Buffer // 要释放的内存地址
);
内存管理最佳实践:
- 分配内存后立即检查返回值,使用
EFI_ERROR()宏判断。 - 所有分配的内存必须在退出前释放,否则会造成内存泄漏。
- 对于大块内存(>1MB),建议使用
AllocatePages()按页分配,减少内存碎片。 - 使用
ZeroMem()或SetMem()初始化分配的内存。
案例拆解:假设我们需要读取一个文件到内存中处理。文件大小未知,需要先查询大小再分配缓冲区。
UINTN FileSize;
VOID *FileBuffer;
EFI_FILE_PROTOCOL *File;
// 获取文件大小
Status = File->GetPosition(File, &FileSize); // 简化示例,实际需先GetInfo
if (EFI_ERROR(Status)) return Status;
// 分配缓冲区
Status = gBS->AllocatePool(EfiBootServicesData, FileSize, &FileBuffer);
if (EFI_ERROR(Status)) {
Print(L"Failed to allocate %d bytes\n", FileSize);
return Status;
}
// 读取文件内容
Status = File->Read(File, &FileSize, FileBuffer);
if (EFI_ERROR(Status)) {
gBS->FreePool(FileBuffer);
return Status;
}
// 处理数据...
// 释放内存
gBS->FreePool(FileBuffer);
常见误区:不要使用C标准库的malloc()/free(),这些函数在UEFI环境中不可用。也不要混用AllocatePool()和FreePages(),它们管理不同的内存区域。另外,AllocatePool()分配的内存默认未初始化,可能包含随机数据。
4.4 退出服务调用:正确终止UEFI应用程序
核心原理:UEFI应用程序的退出有两种方式:正常返回和调用gBS->Exit()。无论哪种方式,都必须返回一个EFI_STATUS状态码,表示执行结果。Shell环境会根据状态码判断程序是否成功执行。
退出方式对比:
| 退出方式 | 代码 | 适用场景 | 注意事项 |
|---|---|---|---|
| return语句 | return EFI_SUCCESS; | 正常结束,资源已清理 | 适合简单应用 |
| gBS->Exit() | gBS->Exit(ImageHandle, Status, ExitSize, ExitData); | 需要传递退出数据 | Exit后不会返回到调用点 |
| gBS->ExitBootServices() | 不适用于应用程序 | 仅用于OS Loader | 调用后Boot Services不可用 |
退出服务调用规范:
// 方式一:直接返回
EFI_STATUS EFIAPI ShellAppMain(IN UINTN Argc, IN CHAR16 **Argv)
{
// ... 业务逻辑
return EFI_SUCCESS; // 或 EFI_INVALID_PARAMETER 等错误码
}
// 方式二:调用Exit服务
EFI_STATUS EFIAPI UefiMain(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE *SystemTable)
{
EFI_STATUS Status;
// ... 业务逻辑
if (ErrorCondition) {
Status = gBS->Exit(ImageHandle, EFI_DEVICE_ERROR, 0, NULL);
// 注意:Exit不会返回,如果返回则说明调用失败
return Status;
}
return EFI_SUCCESS;
}
退出前的资源清理:
- 释放所有通过
AllocatePool()/AllocatePages()分配的内存。 - 关闭所有打开的文件、协议接口。
- 断开事件通知(
gBS->CloseEvent())。 - 取消定时器(
gBS->SetTimer())。
EFI_SUCCESS时,Shell会显示绿色提示;返回错误码时,Shell会显示红色错误信息并设置%error%变量。合理利用状态码可以增强脚本的自动化处理能力。
案例拆解:开发一个检查系统配置的工具,当发现配置错误时,需要输出错误信息并返回特定错误码。
EFI_STATUS EFIAPI ShellAppMain(IN UINTN Argc, IN CHAR16 **Argv)
{
EFI_STATUS Status;
UINTN BufferSize = 0;
VOID *ConfigData = NULL;
// 读取配置
Status = GetConfigData(&ConfigData, &BufferSize);
if (EFI_ERROR(Status)) {
Print(L"Failed to read config: %r\n", Status);
return Status; // 直接返回错误码
}
// 验证配置
if (!ValidateConfig(ConfigData, BufferSize)) {
Print(L"Config validation failed!\n");
gBS->FreePool(ConfigData);
return EFI_INVALID_PARAMETER; // 返回特定错误码
}
// 处理配置...
gBS->FreePool(ConfigData);
return EFI_SUCCESS;
}
常见误区:不要在退出前调用gBS->ExitBootServices(),这是OS Loader的专属操作。另外,gBS->Exit()调用后不会返回,因此不要在Exit后放置清理代码。如果需要在退出时传递数据,确保ExitData指向的内存是有效的,且ExitSize正确。
总结复盘:UEFI应用程序的框架看似简单,但涉及入口点选择、参数解析、内存管理和退出服务等多个关键环节。开发者需要理解UEFI环境的特殊性——没有标准C库、没有操作系统服务、所有资源必须手动管理。掌握本章内容后,开发者可以编写出结构清晰、健壮可靠的UEFI应用程序。在实际开发中,建议使用EDK2提供的模板和库函数(如UefiLib、ShellLib)来简化开发流程,同时注意遵循UEFI规范的内存管理和退出规范,确保应用程序在各种UEFI固件上都能稳定运行。