uefi_2

UEFI应用程序加载过程

当在Shell中执行UefiMain.efi时,Shell首先使用gBS->LoadImage()将UefiMain.efi文件加载到内存生成Image对象,然后调用gBS->StartImage(Image)启动这个Image对象。

1
2
3
4
5
6
7
8
9
// 用于通过设备路径加载和运行镜像,EFI_SUCCESS命令执行成功,EFI_INVALID_PARAMETER参数无效,EFI_OUT_OF_RESOURCES资源耗尽,EFI_UNSUPPORTED不允许嵌套shell调用
EFI_STATUS
InternalShellExecuteDevicePath (
IN CONST EFI_HANDLE *ParentImageHandle, // 执行指定的image handle
IN CONST EFI_DEVICE_PATH_PROTOCOL *DevicePath, // 执行文件的设备路径
IN CONST CHAR16 *CommandLine OPTIONAL, // 指向包含命令行的UCS-2编码的字符串,NULL结尾
IN CONST CHAR16 **Environment OPTIONAL, // 环境变量数组,NULL结尾,格式为"x=y",其中x是环境变量名,y是值,为NULL则使用当前Shell环境
OUT EFI_STATUS *StartImageStatus OPTIONAL // 从gBS->StartImage()返回的状态
)

第一步:将UefiMain.efi文件加载到内存,生成Image对象,NewHandle是这个对象的句柄。

1
2
3
4
5
6
7
8
9
10
// 加载镜像时,FALSE表示不从boot manager加载,NULL和0表示镜像尚未加载到内存中
Status = gBS->LoadImage (FALSE, *ParentImageHandle, (EFI_DEVICE_PATH_PROTOCOL *)DevicePath, NULL, 0, &NewHandle);
if (EFI_ERROR (Status)) {
// 在NewHandle不为NULL(参数无效等情况下NewHandle为NULL就不需要卸载镜像)后卸载镜像并释放通过AllocatePool()为NewCmdLine分配的内存
if (NewHandle != NULL) {
gBS->UnloadImage (NewHandle);
}
FreePool (NewCmdLine);
return (Status);
}

函数指针LoadImage()指向函数CoreLoadImage(),函数内部再调用CoreLoadImageCommon()将EFI镜像加载到内存中,并返回镜像句柄,函数内部会优先判断是从内存加载还是从设备加载,如果SourceBuffer不为NULL,则返回指向内存位置的指针,此时DevicePath仅作为标识,不参与实际读取,SourceBuffer为NULL是最常见的Shell加载方式,此时会按照FirmwareVolume2(BIOS芯片内部的固件卷),SimpleFileSystem(文件系统),LoadFile2和LoadFile(PXE网络启动等没有本地存储)的顺序解析HandleFilePath,通过设备路径获取源文件指针并保存到FHand.Source。

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
// 如果传递了文件副本,就直接使用
if (SourceBuffer != NULL) {
FHand.Source = SourceBuffer;
FHand.SourceSize = SourceSize;
Status = CoreLocateDevicePath (&gEfiDevicePathProtocolGuid, &HandleFilePath, &DeviceHandle);
if (EFI_ERROR (Status)) {
DeviceHandle = NULL;
}
// 这里的错误处理后没有使用goto Done,因为SourceBuffer的优先级是高于DevicePath的,如果文件大小没问题,状态会被重置回EFI_SUCCESS,加载流程仍将继续
if (SourceSize > 0) {
Status = EFI_SUCCESS;
} else {
Status = EFI_LOAD_ERROR;
}
} else {
if (FilePath == NULL) {
// SourceBuffer和DevicePath都为NULL则直接返回
return EFI_INVALID_PARAMETER;
}
// 尝试通过检查匹配协议来获取镜像设备的句柄
Node = NULL;
Status = CoreLocateDevicePath (&gEfiFirmwareVolume2ProtocolGuid, &HandleFilePath, &DeviceHandle);
if (!EFI_ERROR (Status)) {
// 针对FV需要特殊处理
ImageIsFromFv = TRUE;
} else {
HandleFilePath = FilePath;
Status = CoreLocateDevicePath (&gEfiSimpleFileSystemProtocolGuid, &HandleFilePath, &DeviceHandle);
if (EFI_ERROR (Status)) {
if (!BootPolicy) {
HandleFilePath = FilePath;
Status = CoreLocateDevicePath (&gEfiLoadFile2ProtocolGuid, &HandleFilePath, &DeviceHandle);
}
if (EFI_ERROR (Status)) {
HandleFilePath = FilePath;
Status = CoreLocateDevicePath (&gEfiLoadFileProtocolGuid, &HandleFilePath, &DeviceHandle);
if (!EFI_ERROR (Status)) {
// 针对PXE启动环境需要特殊处理
ImageIsFromLoadFile = TRUE;
Node = HandleFilePath;
}
}
}
}
// 通过设备路径获取源文件缓冲区
FHand.Source = GetFileBufferByFilePath (BootPolicy, FilePath, &FHand.SourceSize, &AuthenticationStatus);
if (FHand.Source == NULL) {
Status = EFI_NOT_FOUND;
} else {
// 标记内存用于临时存放文件数据,PE镜像被解析并完成移动后临时内存将被释放
FHand.FreeBuffer = TRUE;
// 针对PXE网络启动的环境需要重新修正完整路径,第二个文件节点路径将被附加到第一个网络设备路径,从而创建一个新的设备路径
if (ImageIsFromLoadFile) {
// LoadFile()可能会导致句柄的设备路径更新
OriginalFilePath = AppendDevicePath (DevicePathFromHandle (DeviceHandle), Node);
if (OriginalFilePath == NULL) {
// OriginalFilePath为NULL,说明新分配的缓冲区内存不足,返回EFI_OUT_OF_RESOURCES表示资源不足Image未能加载
Image = NULL;
Status = EFI_OUT_OF_RESOURCES;
goto Done;
}
}
}

读取到文件后为其构建文件对象,注意分配大小是LOADED_IMAGE_PRIVATE_DATA,其结构包含类型为EFI_LOADED_IMAGE_PROTOCOL的Info字段。

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
// 分配新的镜像结构
Image = AllocateZeroPool (sizeof (LOADED_IMAGE_PRIVATE_DATA));
if (Image == NULL) {
// 内存分配失败,goto Done处理错误
Status = EFI_OUT_OF_RESOURCES;
goto Done;
}
// 仅提取LoadedImage文件路径的设备路径中的文件部分
FilePath = OriginalFilePath;
if (DeviceHandle != NULL) {
Status = CoreHandleProtocol (DeviceHandle, &gEfiDevicePathProtocolGuid, (VOID **)&HandleFilePath);
if (!EFI_ERROR (Status)) {
FilePathSize = GetDevicePathSize (HandleFilePath) - sizeof (EFI_DEVICE_PATH_PROTOCOL);
FilePath = (EFI_DEVICE_PATH_PROTOCOL *)(((UINT8 *)FilePath) + FilePathSize);
}
}
// 初始化内部驱动程序的字段
Image->Signature = LOADED_IMAGE_PRIVATE_DATA_SIGNATURE;
Image->Info.SystemTable = gDxeCoreST;
Image->Info.DeviceHandle = DeviceHandle;
Image->Info.Revision = EFI_LOADED_IMAGE_PROTOCOL_REVISION;
Image->Info.FilePath = DuplicateDevicePath (FilePath);
Image->Info.ParentHandle = ParentImageHandle;
if (NumberOfPages != NULL) {
Image->NumberOfPages = *NumberOfPages;
} else {
Image->NumberOfPages = 0;
}

安装镜像又分为四步,任何一步出错会通过goto Done处理,CoreInstallProtocolInterfaceNotify()用于在Boot Services环境安装协议接口,此时初步挂载gEfiLoadedImageProtocolGuid到Image->Handle,但不加载PE;CoreLoadPeImage()开始加载、重定位并调用PE/COFF镜像(实际内部并没有任何函数调用PE镜像),函数内部通过PeCoffLoaderGetImageInfo()获取PE镜像信息,PeCoffLoaderLoadImage()将镜像加载到分配的内存中,PeCoffLoaderRelocateImage()重定位在内存中的镜像,在确认可用的条件下填充EntryPoint作为返回结果;CoreReinstallProtocolInterface()在设备句柄重新安装协议接口,协议的旧接口被新接口替换,内部会调用CoreNotifyProtocolEntry()以触发通知;CoreInstallProtocolInterface()本身作为公共EFIAPI,内部会调用CoreInstallProtocolInterfaceNotify()重新为Image->Handle安装LoadedImageDevicePath协议。

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
// 安装镜像的协议接口,暂不触发通告
Status = CoreInstallProtocolInterfaceNotify (&Image->Handle, &gEfiLoadedImageProtocolGuid, EFI_NATIVE_INTERFACE, &Image->Info, FALSE);
if (EFI_ERROR (Status)) {
goto Done;
}
// 加载镜像,如果EntryPoint为NULL则不会被设置
Status = CoreLoadPeImage (BootPolicy, &FHand, Image, DstBuffer, EntryPoint, Attribute);
if (EFI_ERROR (Status)) {
if ((Status == EFI_BUFFER_TOO_SMALL) || (Status == EFI_OUT_OF_RESOURCES)) {
if (NumberOfPages != NULL) {
*NumberOfPages = Image->NumberOfPages;
}
}
goto Done;
}
if (NumberOfPages != NULL) {
*NumberOfPages = Image->NumberOfPages;
}
// 重新安装已加载的镜像协议以触发任何通知
Status = CoreReinstallProtocolInterface (Image->Handle, &gEfiLoadedImageProtocolGuid, &Image->Info, &Image->Info);
if (EFI_ERROR (Status)) {
goto Done;
}
// 为PE/COFE镜像的镜像句柄安装LoadedImageDevicePath协议
Status = CoreInstallProtocolInterface (&Image->Handle, &gEfiLoadedImageDevicePathProtocolGuid, EFI_NATIVE_INTERFACE, Image->LoadedImageDevicePath);
if (EFI_ERROR (Status)) {
goto Done;
}
// 成功返回镜像句柄
*ImageHandle = Image->Handle;

第二步:取得命令行参数,并将命令行参数交给UefiMain.efi的Image对象,即NewHandle。

1
2
3
4
5
6
7
8
9
10
Status = gBS->OpenProtocol (NewHandle, &gEfiLoadedImageProtocolGuid, (VOID **)&LoadedImage, gImageHandle, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL);
if (!EFI_ERROR (Status)) {
// 如果镜像不是应用,终止操作
if (LoadedImage->ImageCodeType != EfiLoaderCode) {
ShellPrintHiiDefaultEx (
STRING_TOKEN (STR_SHELL_IMAGE_NOT_APP),
ShellInfoObject.HiiHandle
);
goto UnloadImage;
}

第三步:启动所加载的Image。

1
2
3
4
5
6
7
8
9
if (!EFI_ERROR (Status)) {
StartStatus = gBS->StartImage (NewHandle, 0, NULL);
if (StartImageStatus != NULL) {
*StartImageStatus = StartStatus;
}
CleanupStatus = gBS->UninstallProtocolInterface (NewHandle, &gEfiShellParametersProtocolGuid, &ShellParamsProtocol);
ASSERT_EFI_ERROR (CleanupStatus);
goto FreeAlloc;
}

函数指针StartImage()指向函数CoreStartImage(),函数内部将控制权转移到已加载镜像的入口点,通过AllocatePool()申请内存,使用ALIGN_POINTER保证JumpContext符合CPU架构的内存对齐要求,对于X64的函数调用规则,SetJump()会负责将需要的Rbx,Rsp,Rbp,Rdi,Rsi,R12,R13,R14,R15,Rip,MxCsr,XmmBuffer(需要注意这里会保存浮点寄存器),Ssp寄存器的内容保存到JumpBuffer,并且首次调用SetJumpFlag为0,控制权的交接在Image->EntryPoint()(是不是觉得这里的ImageHandle和SystemTable很熟悉),而应用程序返回有两种方式,正常结束return EFI_SUCCESS,这样会向下执行CoreExit(),内部调用LongJump()再次回到SetJump(),SetJumpFlag为EFI_SUCCESS,跳过分支执行后续清理流程;强制退出使用gBS->Exit(),内部调用LongJump(),过程同上,唯一不同的是SetJumpFlag为传给Exit()的非0状态码(不理解这里的可以搜索C语言使用setjmp()和longjmp()非局部跳转实现异常处理,两个函数解决goto局部跳转问题,但这里更多是为了避免应用程序陷入兔子洞的情况,这种方法可以将堆栈重置到原来状态而无需解析整个程序的调用链,且拥有返回值说明原因,需要注意除非遇到非常具体的情况,否则最好避免使用它)。

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
// 设置Exit()支持的长跳转JmpContext必须与CPU特定的边界对齐,重新分配缓冲区并进行所需的强制对齐
Image->JumpBuffer = AllocatePool (sizeof (BASE_LIBRARY_JUMP_BUFFER) + BASE_LIBRARY_JUMP_BUFFER_ALIGNMENT);
if (Image->JumpBuffer == NULL) {
// 返回失败后镜像可能会被卸载,此时ImageHandle可能无效,使用NULL句柄记录性能日志
PERF_START_IMAGE_END (NULL);
// 弹出当前起始镜像上下文
mCurrentImage = LastImage;
return EFI_OUT_OF_RESOURCES;
}
Image->JumpContext = ALIGN_POINTER (Image->JumpBuffer, BASE_LIBRARY_JUMP_BUFFER_ALIGNMENT);
SetJumpFlag = SetJump (Image->JumpContext);
// 首次调用SetJump()必须始终返回0,后续调用LongJump()会导致SetJump()返回非0值
if (SetJumpFlag == 0) {
RegisterMemoryProfileImage (Image, (Image->ImageContext.ImageType == EFI_IMAGE_SUBSYSTEM_EFI_APPLICATION ? EFI_FV_FILETYPE_APPLICATION : EFI_FV_FILETYPE_DRIVER));
// 调用镜像入口点
Image->Started = TRUE;
Image->Status = Image->EntryPoint (ImageHandle, Image->Info.SystemTable);
// 如果镜像返回错误,添加调试信息让用户了解情况,并检查驱动程序镜像在这种情况下是否已经释放所有资源
DEBUG_CODE_BEGIN ();
if (EFI_ERROR (Image->Status)) {
DEBUG ((DEBUG_ERROR, "Error: Image at %11p start failed: %r\n", Image->Info.ImageBase, Image->Status));
}
DEBUG_CODE_END ();
// 如果镜像返回,通过Exit()退出
CoreExit (ImageHandle, Image->Status, 0, NULL);
}