UEFI基础知识

UEFI基础知识

UEFI与传统BIOS

一个常见的误解是UEFI是BIOS的替代品。实际上,传统主板和基于UEFI的主板都带有BIOS ROM,其中包含在将某些第三方代码加载到内存并跳转到内存之前执行系统初始开机配置的固件。传统BIOS固件和UEFI BIOS固件之间的区别在于它们找到代码,在跳转到系统之前如何准备系统,以及它们为运行时调用的代码提供了哪些便利功能。

平台初始化

在传统系统上,BIOS执行所有常规平台初始化(内存控制器配置,PCI总线配置和BAR映射,图形卡初始化等),但随后进入向后兼容的实模式环境。引导加载程序必须启用A20门,配置GDT和IDT,切换到保护模式,对于x86-64 CPU,配置分页并切换到长模式。

UEFI固件执行相同的步骤,但也为具有平面分段的受保护模式环境和x86-64 CPU(具有身份映射分页的长模式环境)做好准备。A20门也启用。

另外,UEFI固件的平台初始化过程是标准化的。这允许UEFI固件以与供应商无关的方式进行扩展。

启动机制

传统BIOS将来自引导设备的MBR的512字节平坦二进制blob加载到物理地址7C00的内存中并跳转到它。引导加载程序无法返回BIOS。UEFI固件将任意大小的UEFI应用程序(可重定位的PE可执行文件)从GPT分区的引导设备上的FAT分区加载到在运行时选择的某个地址。然后它调用该应用程序的主要入口点。应用程序可以将控制权返回给固件,固件将继续搜索其他引导设备或显示诊断菜单。

系统发现

传统引导加载程序扫描内存以查找EBDA,SMBIOS和ACPI表等结构。它使用PIO与根PCI控制器通信并扫描PCI总线。内存中可能存在冗余表(例如,SMBIOS中的MP表包含也存在于ACPI DSDT中的信息),并且引导加载程序可以选择使用哪些表。

当UEFI固件调用UEFI应用程序的入口点函数时,它会传递“系统表”结构,该结构包含指向所有系统的ACPI表,存储器映射以及与OS相关的其他信息的指针。传统表(如SMBIOS)可能不存在于内存中。

便利功能

传统BIOS挂起各种中断,引导加载程序可以触发这些中断来访问磁盘和屏幕等系统资源。除历史惯例外,这些中断不是标准化的。每个中断使用不同的寄存器传递约定。

UEFI固件在内存中建立了许多可调用函数,这些函数被分组为称为“协议”的集合,并且可通过系统表发现。每个协议中每个函数的行为由规范定义。UEFI应用程序可以定义自己的协议并将它们保存在内存中以供其他UEFI应用程序使用。使用许多C编译器支持的标准化现代调用约定调用函数。

环境

传统引导加载程序可以在任何可以生成平面二进制映像的环境中开发:NASM,GCC等.UEFI应用程序可以用任何语言开发,可以编译并链接到PE可执行文件,并支持用于访问已建立函数的调用约定在UEFI固件的内存中。实际上,这意味着两种开发环境之一:Intel的TianoCore EDK2或GNU-EFI。

TianoCore是一个庞大而复杂的环境,拥有自己的构建系统。它可以配置为使用GCC,MinGW,Microsoft Visual C ++等作为交叉编译器。它不仅可以用于编译UEFI应用程序,还可以用于编译要闪存到UE ROM的UEFI固件。

GNU-EFI是一组库和头文件,用于使用系统的本机GCC编译UEFI应用程序。它不能用于编译UEFI固件。由于它只是一个可以链接UEFI应用程序的库,因此它比TianoCore更容易使用。

仿真

Bochs附带默认的开源传统BIOS。此外,SeaBIOS是一种流行的开源传统BIOS,已被移植到Bochs和QEMU仿真机器上。这两个BIOS都实现了您期望的大多数旧版BIOS功能。但是,它们在操作上与真实机器上的商业传统BIOS相差很大。

OVMF是一种流行的开源UEFI固件,已经移植到QEMU(但不是Bochs)仿真机。因为它实现了UEFI规范,所以它的行为与真实机器上的商用UEFI固件非常相似。(OVMF本身是使用TianoCore构建的,但可以使用预先构建的图像。)

传统引导加载程序或UEFI应用程序?

如果您要针对UEFI不可用或不可靠的旧系统,则应开发旧版引导加载程序。这需要深入了解16位寻址和x86或x86-64 CPU的向后兼容性功能。如果您的目标是现代系统,则应开发UEFI应用程序。许多UEFI固件可以配置为模拟传统BIOS,但这些模拟环境之间的差异甚至比真正的传统BIOS更多。

虽然熟悉UEFI开发环境,使用系统表以及访问UEFI提供的协议(功能)有一点学习曲线,但与尝试保持与各种各样的快速兼容的“陷阱”要少得多 - 在真机上成为过时的传统BIOS。UEFI是所有现代PC的标准。

UEFI等级0-3和CSM

PC被分类为UEFI类0,1,2或3.类0机器是具有传统BIOS的传统系统; 即根本不是UEFI系统。

1类机器是仅在兼容性支持模块(CSM)模式下运行的UEFI系统。CSM是UEFI固件如何模拟传统BIOS的规范。CSM模式下的UEFI固件加载传统的引导加载程序。类1 UEFI系统可能根本不通告UEFI支持,因为它没有暴露给引导加载程序。它只是UEFI“在BIOS内部”。

2类机器是UEFI系统,可以启动UEFI应用程序,但也包括在CSM模式下运行的选项。大多数现代PC都是UEFI 2级机器。有时,选择运行UEFI应用程序与CSM是BIOS配置中的一个或另一个设置,有时BIOS将决定在选择引导设备并检查它是否具有传统引导加载程序或UEFI应用程序。

3类机器是不支持CSM的UEFI系统。UEFI class 3机器仅运行UEFI应用程序,并且不实现CSM以向后兼容传统引导加载程序。

安全启动

Secure Boot是UEFI应用程序的数字签名方案,由四个组件组成:

  • PK:平台密钥
  • KEK:密钥交换密钥
  • db:白名单数据库
  • dbx:黑名单数据库

支持安全启动的UEFI固件始终处于以下三种状态之一:

  • 设置模式,安全启动关闭
  • 用户模式,安全启动关闭
  • 用户模式,安全启动上

在设置模式下,任何UEFI应用程序都可以更改或删除PK,从KEK添加/删除密钥,以及从db或dbx添加/删除白名单或黑名单条目。

在用户模式下,无论安全启动是打开还是关闭:

  • PK只能由已具有当前PK的UEFI应用程序更改或删除。
  • 只能通过具有PK的UEFI应用程序从KEK添加/删除密钥。
  • 白名单和黑名单条目只能由具有KEK中任何一个密钥的UEFI应用程序在db和dbx中添加/删除。

最后,在启用安全启动的用户模式下,UEFI应用程序必须满足以下四个要求之一:

  • 签名,在db中签名,而不在dbx中签名
  • 由db中的密钥签名而不是dbx中的密钥
  • 由KEK的一把钥匙签名
  • 无符号,但应用程序的哈希值在db中而不在dbx中

请注意,除非PK恰好位于KEK中,否则UEFI应用程序不会被PK签名。

并非所有UEFI固件都支持安全启动,尽管它是Windows 10的要求。某些UEFI固件支持安全启动并且不允许它被禁用,这对于无法访问PK或任何KEK中的密钥,因此无法将自己的密钥或应用程序签名或哈希值安装到白名单数据库中。独立开发人员应在不支持安全启动或允许关闭安全启动的系统上进行开发。

如何使用UEFI

Windows和Linux等传统操作系统具有现有的软件体系结构和大型代码库,可用于执行系统配置和设备发现。凭借其复杂的抽象层,他们无法直接受益于UEFI。因此,他们的UEFI引导加载程序做的很少,但为它们准备运行环境。

独立开发人员可能会发现使用UEFI编写功能完备的UEFI应用程序更有价值,而不是将UEFI视为在引导过程中被抛弃的临时启动环境。与通常仅与BIOS交互以启动OS的传统引导加载程序不同,UEFI应用程序可以在UEFI的帮助下实现复杂的行为。换句话说,独立开发者不应急于离开“UEFI-land”。

一个很好的起点是编写一个使用系统表来获取内存映射的UEFI应用程序,并使用“文件”协议从FAT格式的磁盘中读取文件。下一步可能是使用系统表来查找ACPI表。

使用GNU-EFI进行开发

GNU-EFI可用于开发32位和64位UEFI应用程序。本节仅讨论64位UEFI应用程序,并假设开发环境本身在x86_64系统上运行,因此不需要交叉编译器。有关正确开发环境的更全面介绍,请参阅UEFI Bare Bones。

GNU-EFI包括四件事:

  • crt0-efi-x86_64.o:CRT0(C运行时初始化代码),提供UEFI固件在启动应用程序时将调用的入口点,该入口点又将调用开发人员写入的“efi_main”函数。
  • libgnuefi.a:包含CRT0使用的单个函数(_relocate)的库。
  • elf_x86_64_efi.lds:用于链接UEFI应用程序的链接描述文件。
  • efi.h和其他标头:提供结构,typedef和常量的便捷标头在访问系统表和其他UEFI资源时提高了可读性。
  • libefi.a:包含便捷函数的库,如CRC计算,字符串长度计算和简单文本打印。
  • efilib.h:报头为:libefi.a。

至少,64位UEFI应用程序需要使用elf_x86_64_efi.lds链接描述文件链接crt0-efi-x86_64.o和libgnuefi.a。您很可能也希望使用提供的标题和便利库,本节将假设您继续使用。传统的“Hello,world”UEFI程序如下所示。

#include <efi.h> 
#include <efilib.h>
 
EFI_STATUS
EFIAPI
efi_main ( EFI_HANDLE ImageHandle , EFI_SYSTEM_TABLE * SystemTable )
{ 
  InitializeLib ( ImageHandle , SystemTable ); 
  打印( L “Hello,world!\ n ” ); 
  返回 EFI_SUCCESS ; 
}

几点说明:

  • 包含efi.h,因此我们可以使用EFI_STATUS,EFI_HANDLE和EFI_SYSTEM_TABLE等类型。
  • 创建32位UEFI应用程序时,EFIAPI为空; GCC将使用标准C调用约定编译“efi_main”函数。创建64位UEFI应用程序时,EFIAPI扩展为“__attribute __((ms_abi))”,GCC将使用Microsoft的x64调用约定编译“efi_main”函数,如UEFI所指定。只有直接从UEFI(即main)调用的函数才需要使用UEFI调用约定。
  • “InitializeLib”和“Print”是libefi.a在efilib.h中提供原型的便捷函数。“InitializeLib”允许libefi.a存储对BIOS提供的ImageHandle和SystemTable的引用。“Print”使用那些存储的引用来通过到达内存中UEFI提供的函数来打印字符串。(稍后我们将看到如何手动查找和调用UEFI提供的功能。)

该程序编译和链接如下。

$ gcc main.c \
       -c                                  \
       -fno-stack-protector                \
       -fpic                               \
       -fshort-wchar                       \
       -mno-red-zone                       \
       -I  / path / to / gnu-efi / headers \
       -I  / path / to / gnu-efi / headers / x86_64 \
      -DEFI_FUNCTION_WRAPPER \
      -o main.o
 
$ LD main.o \
      /路径/到/信息crt0-EFI-x86_64.o \
      -nostdlib                       \
      -znocombreloc                   \
      -T  /路径/到/ elf_x86_64_efi.lds \
      -shared                         \
      -Bsymbolic                      \
      -L  /路径/到/库\
     -l:libgnuefi.a \
     -l:libefi.a \
     -o main.so
 
$ objcopy -j .text \
           -j .sdata \
           -j .data \
           -j .dynamic \
           -j .dynsym \
           -j .rel \
           -j .rela \
           -j .reloc \
           --target = efi-app- x86_64 \
          主要的
          main.efi

此过程的结果是44 kB PE可执行文件main.efi。在一个真实的项目中,您可能希望使用make或其他构建工具,并且可能需要构建交叉编译器。

使用QEMU和OVMF进行仿真

具有最新版本的OVMF的任何最新版本的QEMU都足以运行UEFI应用程序。QEMU二进制文件可用于许多平台,二进制OVMF映像(OVMF.fd)可以在TianoCore网站上找到。QEMU(没有任何启动盘)可以调用如下。(为防止最新版本的QEMU在未找到引导磁盘时尝试PXE(网络)引导,请使用-net none。

使用OVMF(适用于QEMU 1.6或更高版本)的推荐方法是使用pflash参数。下面的说明假设您将OVMF映像拆分为单独的CODE和VARS部分。

$ qemu-system-x86_64 -cpu qemu64 \
   -drive  if = pflash,format = raw,unit = 0,file = path_to_OVMF_CODE.fd,readonly = on \
   -drive  if = pflash,format = raw,unit = 1,file = path_to_OVMF_VARS.fd \
   -net none

如果您更喜欢在没有显示器的终端上工作,或者通过SSH / telnet工作,您将需要使用-nographic标志运行QEMU而不支持图形。

如果OVMF找不到具有正确命名的UEFI应用程序的引导磁盘(稍后会详细介绍),它将进入UEFI shell。
在这里插入图片描述

创建磁盘映像

要启动UEFI应用程序,您需要创建一个磁盘映像并将其呈现给QEMU。UEFI固件期望UEFI应用程序存储在GPT分区磁盘上的FAT12,FAT16或FAT32文件系统中。许多固件仅支持FAT32,因此您需要使用它。根据您的平台,有几种不同的方法可以创建包含UEFI应用程序的磁盘映像,但它们都是从创建归零磁盘映像文件开始的。最小FAT32分区大小为33,548,800字节,此外,您将需要主GPT表和辅助GPT表的空间,以及一些空闲空间,以便可以正确对齐分区。在这些示例中,我们将创建一个48,000,000字节(93750 512字节扇区,或48 MB)磁盘映像。

$ dd  if = / dev / zero of = / path / to / uefi.img bs = 512  count = 93750
uefi-run帮助应用程序

uefi运行的应用程序对快速测试很有用。它会创建一个包含EFI应用程序的临时FAT映像并启动qemu。

$ uefi-run -b  / path / to / OVMF.fd -q  / path / to / qemu app.efi -  < extra_qemu_args >

uefi-run目前尚未打包用于任何发行版。您可以使用货物(Rust包管理器)安装它(“货物安装uefi-run”)。

Linux,root必需

此方法需要root权限并使用gdisk,losetup和mkdosfs。首先,使用gdisk创建具有单个EFI系统分区的GPT分区表。

$ gdisk / path / to / uefi.img
GPT fdisk ( gdisk )版本0.8.10
 
分区表扫描:
  MBR:不存在
  BSD:不在场
  APM:不在场
  GPT:不存在
 
创建新的GPT条目。
 
命令(?用于 帮助):○
此选项删除所有分区并创建新的保护MBR。
继续?(是/否):y
 
命令(?用于 帮助):N
分区编号(1 - 128,默认值1 ):1 
第一个扇区(34 - 93716,默认= 2048 )或{ +  - } 大小{ KMGTP }:2048 
最后一个扇区(2048 - 93716,默认= 93716 )或{ +  - } size { KMGTP }:93716 
当前类型是'Linux filesystem'
十六进制代码或GUID ( L代表显示代码,输入= 8300 ):ef00将分区类型 
更改为“EFI System”
 
命令(?用于 帮助):w ^
 
最终检查完成。即将编写 GPT数据。这将覆盖现有的
分数!!
 
你想继续吗?(是/否):y
好; 将新的GUID分区表( GPT )写入uefi.img。
警告:内核仍在使用旧分区表。
新表将在下次重新启动时使用。
该操作已成功完成。

现在你有一个带有GUID分区表的磁盘映像和一个从扇区2048开始的无格式EFI分区。除非你偏离上面显示的命令,否则磁盘映像将使用512字节扇区,因此EFI分区从字节1,048,576开始并且是长度为46,934,528字节。使用losetup在环回设备上向Linux提供分区。

losetup --offset  1048576  --sizelimit  46934528  / dev / loop0 / path / to / uefi.img

(如果/ dev / loop0已在使用中,则需要选择不同的环回设备。)

使用mkdosfs格式化FAT32的分区。

mkdosfs -F  32  / dev / loop0

现在可以安装分区,以便我们可以将文件复制到它。在此示例中,我们使用“/ mnt”目录,但您也可以创建一个临时使用的本地目录。

mount  / dev / loop0 / mnt

将要测试的任何UEFI应用程序复制到文件系统。

$ cp  / path / to / main.efi / mnt / 
$ ...

最后,卸载分区并释放回送设备。

$ umount  / mnt
$ losetup -d  / dev / loop0

uefi.img现在是一个包含主要和次要GPT表的磁盘映像,其中包含EFI类型的单个分区,其中包含FAT32文件系统,其中包含一个或多个UEFI应用程序。

Linux,root不是必需的

此方法使用parted,mformat和mcopy,可以使用用户权限执行。首先,使用parted创建主要和次要GPT标头,并使用与上述方法相同范围的单个EFI分区。

$ parted / path / to / uefi.img -s  -a minimal mklabel gpt
$ parted / path / to / uefi.img -s  -a minimal mkpart EFI FAT16 2048s 93716s
$ parted / path / to / uefi.img -s  -a minimal toggle 1 boot

现在创建一个包含EFI分区数据的新临时映像文件,并使用mformat将其格式化为FAT16。

dd  if = / dev / zero of = / tmp / part.img bs = 512  count = 91669 
mformat -i  / tmp / part.img -h  32  -t  32  -n  64  -c  1

使用mcopy将要测试的任何UEFI应用程序复制到文件系统。

$ mcopy -i  / tmp / part.img / path / to / main.efi ::
$ ...

最后,将分区映像写入主磁盘映像。

$ DD  如果 = / TMP / part.img 的 = /路径/到/ uefi.img BS = 512  计数 = 91669  寻求 = 2048  CONV = notrunc之外

uefi.img现在是一个包含主要和次要GPT表的磁盘映像,包含EFI类型的单个分区,包含FAT16文件系统,包含一个或多个UEFI应用程序。

FreeBSD,需要root

此方法需要root权限,并使用mdconfig,gpart,newfs_msdos和mount_msdosfs。首先,创建一个设备节点,将零磁盘映像显示为块设备。这将让我们使用标准分区和格式化工具来处理它。

$ mdconfig -f  / path / to / uefi.img
MD0

在此示例中,新块设备是md0。现在在设备上创建空的主要和辅助GPT表。

$ gpart create -s GPT md0
md0已创建

现在我们可以在磁盘上添加一个分区。我们将指定一个“EFI”分区,这意味着GPT会将该分区的GUID设置为特殊的“EFI”类型。并非所有的BIOS都需要这个,并且仍然可以在Linux,FreeBSD和Windows上正常挂载和浏览分区。

$ gpart add -t efi md0
md0p1补充道

接下来,在新分区上创建FAT16文件系统。如果您愿意,可以为文件系统指定各种参数,但这不是必需的。理想情况下,您将创建一个FAT32分区以获得最佳固件兼容性,但FreeBSD似乎创建了OVMF无法读取的FAT32分区。

$ newfs_msdos -F  16 md0p1
newfs_msdos:修剪2个扇区调整到的倍数9 
/ dev的/ md2p1:93552个扇区在 11694个 FAT16簇(4096个字节/簇)
BytesPerSec = 512  SecPerClust = 8  ResSectors = 1  的FAT = 2  RootDirEnts = 512个 媒体 = 0XF0 FATsecs = 46  SecPerTrack = 9  Heads = 16  HiddenSecs =0  HugeSectors = 93681

现在可以安装分区,以便我们可以将文件复制到它。在本例中,我们使用/ mnt目录,但您也可以创建一个临时使用的本地目录。

$ mount_msdosfs / dev / md0p1 / mnt

将要测试的任何UEFI应用程序复制到文件系统。

$ cp  / path / to / main.efi / mnt / 
$ ...

最后,卸载分区并释放块设备。

uefi.img现在是一个包含主要和次要GPT表的磁盘映像,包含EFI类型的单个分区,包含FAT16文件系统,包含一个或多个UEFI应用程序。

Mac OS(根不需要)

Mac OS有一个工具(hdiutil),可以同时创建磁盘映像和复制文件。

假设您正在为x86_64创建UEFI引导。根据定义,文件名应为BOOTX64.EFI,此文件应位于/ EFI / BOOT文件夹中。

首先,让我们创建一个临时文件夹,其中包含引导UEFI所需的所有文件和文件夹。

$ mkdir  -p diskImage / EFI / BOOT

其次,让我们将启动应用程序复制到所需的位置:

$ cp bootx64.efi diskImage / EFI / BOOT / BOOTX64.EFI

最后,让我们创建一个用GPT分区的磁盘映像,格式化为fat32(-fs fat32),如果需要覆盖目标文件(-ov),定义磁盘大小(-size 48m),定义卷名(-volname NEWOS),文件磁盘将被编码的格式(-format UDTO - 与DVD / CD相同)和包含将复制到新磁盘的文件的源文件夹:

$ hdiutil create -fs fat32 -ov  -size 48m -volname NEWOS -format UDTO -srcfolder diskImage uefi.cdr

uefi.cdr应该准备好供QEMU使用。

启动UEFI应用程序

磁盘映像准备就绪后,您可以按如下方式调用QEMU。

$ qemu-system-x86_64 -cpu qemu64 -bios  / path / to / OVMF.fd -drive  file = uefi.disk,if = ide

当OVMF进入UEFI shell时,您将在“映射表”中看到标记为“FS0”的附加条目。这表示固件检测到磁盘,发现了分区,并且能够挂载文件系统。您可以使用DOS样式语法“FS0:”切换到文件系统,如下图所示。
在这里插入图片描述
您可以通过输入其名称来启动UEFI应用程序。
在这里插入图片描述
请注意,一旦应用程序终止,UEFI shell将恢复。当然,如果这是一个合适的引导加载程序,它永远不会恢复,而是启动操作系统。

一些商用UEFI固件提供UEFI外壳或启动用户选择的UEFI应用程序的能力,例如HP EliteBook系列笔记本电脑附带的固件。但是,大多数人不会将此功能暴露给最终用户。

调试

OVMF可以在调试模式下构建,它会将日志消息输出到IO端口0x402。您可以使用下面的一些标志来捕获输出。

-debugcon file:uefi_debug.log -global isa-debugcon.iobase=0x402

请注意,发布版本不会输出调试消息,也不会减少输出。

请参阅使用GDB调试UEFI应用程序。

在真实硬件上运行

NVRAM变量

主要文章: UEFI NVRAM
UEFI固件将通过文本或图形配置菜单呈现其大多数配置选项,就像传统BIOS一样。在这些菜单中进行的选择在重新启动之间存储在NVRAM芯片中。然而,与传统BIOS不同,固件开发人员可以选择通过启动时固件驻留在RAM中的便捷功能将部分或全部这些“NVRAM变量”暴露给OS和最终用户。

Linux efivarfs内核模块将使用这些函数列出/ sys / firmware / efi / efivars文件中的NVRAM变量。也可以使用dmpstore命令从UEFI shell本身转储NVRAM变量。始终可以通过NVRAM变量访问设备引导顺序。Linux程序efibootmgr专门用于引导顺序NVRAM变量。UEFI shell提供bcfg命令用于相同目的。

可启动的UEFI应用程序

引导顺序NVRAM变量确定固件在启动时要查找的UEFI应用程序的位置。虽然可以更改(例如,OS安装程序可能会自定义安装它的硬盘驱动器的引导条目),但固件通常会查找名为“BOOT.efi”的UEFI应用程序(对于32位应用程序)或“BOOTX64” .efi“(对于64位应用程序)存储在引导设备的文件系统中的”/ EFI / BOOT“路径中。这是OVMF的默认路径和名称。

与从shell启动的UEFI应用程序不同,如果可引导的UEFI应用程序返回BIOS,则将继续搜索其他引导设备。

暴露的功能

真实PC在向用户公开的UEFI能力方面有所不同。例如,即使是3级机器也可能在其BIOS配置中没有提及UEFI,并且可能不提供UEFI shell。此外,一些BIOS供应商使其UEFI固件配置屏幕看起来与其传统BIOS配置屏幕完全相同。2类机器可能会出现一些令人困惑的启动菜单和配置选项。例如,一个笔记本电脑制造商包括用于启用/禁用UEFI的配置选项(即,在UEFI和CSM行为之间切换),命名为“OS:Windows 8”。另一台笔记本电脑,如果它无法在所选的引导设备上找到可引导的UEFI应用程序(或者该应用程序返回的状态不是EFI_SUCCESS),则会回退到CSM行为,然后抱怨驱动器的MBR已损坏。随着时间的推移,

要更轻松地在真实硬件上进行测试,您可以将可启动的UEFI应用程序安装到系统的内部硬盘驱动器,该系统提供启动菜单,例如rEFInd。这对于多启动方案也可能是方便的。

PC固件开发人员

在x86和x86-64平台上,以下BIOS开发人员提供UEFI固件:

  • AMI(Aptio)。
  • Phoenix(SecureCore,TrustedCore,AwardCore)。
  • Insyde(InsydeH20)。

Apple系统

与UEFI相反,Apple系统实现EFI 1.0,区别在于UEFI应用程序是从HFS +文件系统而不是FAT12 / 16/32加载的。另外,那些UEFI应用程序必须被“祝福”(直接或通过驻留在受祝福的目录中)来加载。Blessing在加载应用程序之前在Apple的固件检查的HFS +文件系统中设置标志。开源hfsutils包支持在HFS文件系统中祝福文件,但不支持目录和HFS +。

UEFI应用程序详细介绍

二进制格式

UEFI可执行文件是具有特定子系统的常规PE32 / PE32 +(Windows x32 / x64)映像。每个UEFI应用程序基本上都是没有符号表的Windows EXE(或DLL)。

UEFI图像的类型
类型描述
应用OS加载程序和其他实用程序(10)
启动服务驱动程序引导时固件使用的驱动程序(例如磁盘驱动程序,网络驱动程序)(11)
运行时驱动程序即使在操作系统加载和退出引导服务后仍可能保持加载的驱动程序(12)

UEFI映像还必须指定它们包含的机器代码类型。UEFI加载程序将拒绝引导不兼容的映像。

机器类型

名称/拱门 值

名称/拱门
860x014c
x86_64的0x8664
安腾x640200
UEFI字节码0x0EBC
ARM 10x01C2
AArch(ARM x64)0xAA64
RISC-V x320x5032
RISC-V x640x5064
RISC-V x1280x5128

ARM表示您可以使用Thumb / Thumb 2指令,但UEFI接口处于ARM模式。

初始化

应用程序必须加载操作系统并退出引导服务,或者从主函数返回(在这种情况下,引导加载程序将查找要加载的下一个应用程序)。

驱动程序必须初始化,然后在成功时返回0或错误代码。如果所需的驱动程序无法加载,计算机可能会失败。

记忆

UEFI返回的内存映射将标记驱动程序使用的内存区域。

操作系统加载程序完成后,允许内核重用加载引导加载程序的内存。

内存类型是Efi{Loader/BootServices/RuntimeServices}{Code/Data}。

退出引导服务后,您可以重用引导驱动程序使用的任何非只读内存。

但是,永远不要触及运行时驱动程序使用的内存- 只要计算机运行,运行时驱动程序就会保持活动状态并加载。

查看包含UEFI应用程序的PE文件细分的一种方法是

$ objdump --all-headers  / path / to / main.efi

它的输出很长。除此之外,它还显示了子系统,即前面提到的UEFI映像的类型。

召集公约

UEFI指定以下调用约定:

  • cdecl用于x86 UEFI功能
  • 微软针对x86-64 UEFI功能 的64位调用约定
  • 用于ARM UEFI功能的 SMC

这对UEFI应用程序开发人员有两个影响:

  • 期望使用相应的调用约定调用UEFI应用程序的主入口点。
  • 必须使用相应的调用约定来调用UEFI应用程序调用的任何UEFI提供的函数。

请注意,严格应用于应用程序内部的函数可以使用开发人员选择的任何调用约定。

GNU-EFI和GCC

cdecl是GCC使用的标准调用约定,因此在使用GNU-EFI开发的x86 UEFI应用程序中编写主入口点或调用UEFI函数不需要特殊属性或修饰符。但是,对于x86-64,必须使用“___ attribute ___((ms_abi))”修饰符声明入口点函数,并且所有对UEFI提供的函数的调用必须通过“uefi_call_wrapper”thunk进行。使用cdecl调用此thunk,但在调用请求的UEFI函数之前转换为Microsoft x86-64调用约定。这是必要的,因为GCC的旧版本不支持为函数指针指定调用约定。

为方便开发人员,GNU-EFI提供了“EFIAPI”宏,在定位x86时扩展为“cdecl”,在定位x86-64时扩展为“__attribute __(ms_abi)”。此外,“uefi_call_wrapper”thunk将简单地通过x86传递呼叫。这允许相同的源代码以x86和x86-64为目标。例如,以下main函数将在x86和x86-64上使用正确的调用约定进行编译,并且通过“uefi_call_wrapper”thunk调用将选择在调用UEFI函数时使用的正确调用约定(在这种情况下,打印一个串)。

EFI_STATUS EFIAPI efi_main ( EFI_HANDLE ImageHandle , EFI_SYSTEM_TABLE * SystemTable )
{ 
  EFI_STATUS status = uefi_call_wrapper ( SystemTable - > ConOut - > OutputString ,
                                        2 , 
                                        SystemTable - > ConOut , 
                                        L “Hello,World!\ n ” ); 
  返回状态; 
}

语言绑定

UEFI应用程序通常用C语言编写,但可以为编译为机器代码的任何其他语言编写绑定。大会也是一种选择; 一个uefi.inc可用于FASM,使UEFI应用程序进行如下写入。

格式pe64 dll efi
进入主力
 
部“的.text”  代码可执行可读
 
包括 'uefi.inc'
 
主要:
    ; 初始化UEFI库
    InitializeLib
    jc  @f
 
    ; 调用uefi函数打印到屏幕 
    uefi_call_wrapper ConOut , OutputString , ConOut , _hello
 
@@ : mov  eax , EFI_SUCCESS
     retn
 
部“数据”  数据可读可写
 
_hello杜的'Hello World' ,13 ,10 ,0
 
部分'.reloc'修正数据可丢弃

由于UEFI应用程序包含正常的x86或x86-64机器代码,因此内联汇编也是支持它的编译器中的一个选项。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值