UEFI

来自osdev
跳到导航 跳到搜索

UEFI(Unified Extensible Firmware Interface)可扩展固件接口是针对x86、x86-64、ARM和安腾(Itanium)平台的规范,用于定义操作系统和平台固件/BIOS之间的软件接口。 最初的EFI是在20世纪90年代中期由Intel开发的,用于为安腾平台开发固件/BIOS。 2005年,英特尔将该规范移交给一个名为“统一EFI论坛”的新工作组,该工作组由AMD、微软、苹果和英特尔等公司组成。 所有现代PC都附带UEFI固件,UEFI广泛受商业和开源操作系统支持。 并为旧式操作系统提供了向后兼容性。

UEFI基础知识

下载 UEFI 映像

如果你选择VirtualBox进行虚拟化,则已包含UEFI,无需手动下载映像。 你只需在VM的首选项中通过单击'设置'/'系统'/'启用EFI(仅限特殊操作系统)'复选框来启用它。

否则,对于仿真和虚拟机,你将需要一个OVMF.fd固件映像。 它可能很难找到,所以这里也有一些替代的下载链接:

在Linux下,你还可以使用发行版的包管理器安装这些工具,例如:

Debian / Ubuntu

# apt-get install ovmf

RedHat / CentOS

# yum install ovmf

MacOS

使用OVMF-blobs仓库。

Windows

使用OVMF-blobs仓库或下载RPM版本,然后使用7-Zip文件管理器提取OVMF。

UEFI和传统BIOS比较

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

平台初始化

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

UEFI固件执行与传统BIOS相同的步骤,除此以外UEFI还准备了一个具有简单分段的保护模式环境,并为x86-64 CPU准备了一个具有标识映射分页的长模式环境。 A20 gate也已启用。

此外,UEFI固件的平台初始化程序已标准化。这允许UEFI固件以供应商中立的方式进行扩展。

启动机制

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

系统发现

传统BIOS支持的引导加载程序需要扫描内存中的EBDASMBIOSACPI表等结构。 它使用PIO与根PCI控制器通信并扫描PCI总线。 内存中可能存在冗余表(例如,SMBIOS中的 MP表包含同样存在于ACPI DSDTMADT中的信息),引导加载程序可以选择使用哪个。

当UEFI固件调用UEFI应用程序的入口点函数时,它会传递一个“系统表”结构,其中包含指向系统所有ACPI表、内存映射和其他与操作系统相关的信息的指针。 内存中可能不存在旧表(如MP表)。

便利功能

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

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

开发环境

可以在任何可以生成平面二进制映像的环境中开发遗留引导加载程序:NASM、GCC等。 UEFI应用程序可以用任何语言开发,这些语言可以编译并链接到PE可执行文件中,并支持用于访问UEFI固件在内存中建立的功能调用约定。 实际上,这里指的是三种开发环境:EDK2、GNU-EFI或POSIX-UEFI。

EDK2是一个大型、复杂但功能丰富的环境,有自己的构建系统。 它可以配置为使用GCC、LLVM、MinGW、微软Visual C++等 作为交叉编译器。 它不仅可以用来编译UEFI应用程序,还可以用来编译UEFI固件以写入到BIOS ROM。

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

POSIX-UEFI与GNU-EFI非常相似,但它主要作为源代码分发,而不是作为二进制库分发,具有类似ANSI C的名称,并与GCC以及LLVM CLang一起使用。 它附带了一个为你设置编译器标志的Makefile。

模拟

OVMF,一种流行的开源UEFI固件,已经移植到QEMU(Bochs还没有)仿真机器上。 因为它实现了UEFI规范,所以其行为与实际机器上的商用UEFI固件非常相似。 (OVMF本身是由TianoCore建立的,它的源代码也在TianoCore网站上,但可以使用预构建映像。)

选择传统引导加载程序还是UEFI应用程序?

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

尽管在熟悉UEFI开发环境、使用系统表和访问UEFI提供的协议(功能函数)方面有一个很小的学习曲线,但要相比起在真正的机器上与各种迅速过时的传统BIOS兼容,'陷阱'要少得多。 UEFI是所有现代PC的标准。

UEFI 0-3级和CSM

PC被归类为UEFI 0、1、2或3级。0类机器是具有传统BIOS的传统系统,也就是根本不是UEFI系统。

1级机器是一种专门在兼容性支持模块(Compatibility Support Module CSM)模式下运行的UEFI系统。 CSM是UEFI固件如何模拟传统BIOS的规范。 CSM模式下的UEFI固件可以加载旧引导加载程序。 1类UEFI系统可能根本不实现UEFI新功能支持,因为它并不向引导加载程序公开。 它只是BIOS中的UEFI。

2级机器是一种UEFI系统,可以启动UEFI应用程序,但也包括在CSM模式下运行的选项。 大多数现代PC是UEFI 2级机器。 有时选择运行UEFI应用程序还是运行CSM是BIOS配置中的切换设置, 有时BIOS将在选择引导设备时检查其是否具有旧版引导加载程序或UEFI应用程序后,决定使用哪个。

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

安全引导

安全引导是UEFI应用程序的数字签名方案,由四个组件组成:

  • PK: Platform Key
  • KEK: Key Exchange Keys
  • db: Whitelist database
  • dbx: Blacklist database

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

  • 设置模式,安全引导关闭
  • 用户模式,安全引导关闭
  • 用户模式,安全引导开启

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

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

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

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

  • 已签名,签名在db中,而不是在dbx中
  • 由db中的密钥而不是dbx中的密钥签名
  • 用KEK里的钥匙签名
  • 未签名,但应用程序的哈希值在db中,而不是dbx中

请注意,UEFI应用程序不是由PK签名的,除非PK恰好也在KEK中。

并非所有UEFI固件都支持安全引导,尽管这是Windows 8的一项要求。 一些UEFI固件支持安全引导,并且没有禁用的选项,这给无法访问PK或KEK中任何密钥的独立开发人员带来了问题,无法在白名单数据库中安装自己的密钥或应用程序签名或哈希。 独立开发人员应该在不支持安全引导或具有关闭安全引导选项的系统上开发。

一个简单方法是使用由Microsoft签名的加载程序。允许你加载另一个由你拥有的密钥和证书(称为MOK,Machine Owner's Key)签名的二进制文件。 例如这样的载入器有shim,它被RedHat、Fedora、Suse、Ubuntu、Arch和许多其他发行版用来加载GRUB。 EFI可执行文件的文件名在shim中是硬编码的,但是如果将加载程序重命名为GRUBX64.EFI(或GRUBIA32.EFI),你可以使用MOK密钥和证书对其进行签名sbsigntool,然后你可以在安全引导中加载所需的任何加载程序。

尽管某些固件没有关闭安全引导的选项,但Win8的bootmgr.efi中存在巨大的安全漏洞。它允许加载关闭任何计算机上的安全引导的安全策略。 无法修补此缺陷,因为任何人都可以没有任何问题的用原始Win8的bootmgr.efi简单地更换固定的Win10的bootmgr.efi(它们使用相同的Microsoft密钥签名)。

如何使用UEFI

像Windows和Linux这样的传统操作系统有一个现有的软件体系结构和一个庞大的代码库来执行系统配置和设备发现。 由于其已有复杂的抽象层,它们不能直接从UEFI中获益。 因此,他们的UEFI引导加载程序除了为它们的运行准备环境外几乎没有做什么。

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

好的起步方式是编写一个UEFI应用程序,该应用程序使用系统表获取内存映射,并使用“File“协议(译者注:指前面提到的UEFI功能函数库)从FAT格式的磁盘读取文件。 下一步可能是使用系统表来定位ACPI表。

用POSIX-UEFI开发

正文:POSIX-UEFI

在类似POSIX的系统上编译UEFI应用程序的一个选择是使用POSIX-UEFI。 它为你的EFI应用程序提供了一个类似libc的API,并附带了一个Makefile,可以为你检测和设置工具链。 它可以使用GCC或LLVM,默认使用主机编译器,但仍然建议使用交叉编译器。

它使用POSIX风格的typedef(如uintn_t而不是UINTN),并且不附带标准EFI头。 通过从GNU-EFI或EDK2安装EFI头,仍然可以获得POSIX-UEFI(如GOP)未涵盖的接口。 此外,它使用MS ABI进行编译,这意味着只要你的应用程序也使用该ABI进行编译,就可以在本地调用UEFI服务(即不使用uefi_call_wrapper)。 传统的"Hello, world"UEFI程序是这样的。

#include <uefi.h>

int main (int argc, char **argv)
{
printf("Hello, world!\n");
return 0;
}

Makefile如下所示:

TARGET = main.efi
include uefi/Makefile

运行make来构建它。此过程的结果是PE可执行文件main.efi

用GNU-EFI开发

正文:GNU-EFI

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

GNU-EFI包括四项内容:

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

作为最小化起步64位UEFI应用程序需要链接到crt0-efi-x86_64.olibgnuefi.a使用elf_x86_64_efi.lds链接器脚本。 最有可能的情况是,你还需要使用提供的头文件和支持库,本节以下将假设已经完成。

传统的'Hello,world'UEFI程序如下所示。

#include <efi.h>
#include <efilib.h>

EFI_STATUS
EFIAPI
efi_main (EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
{
InitializeLib(ImageHandle, SystemTable);
Print(L"Hello, world!\n");
return 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将按照UEFI的规定,使用Microsoft的x64调用约定编译'efi_main'函数。 只有将直接从UEFI调用的函数需要使用UEFI调用约定,这些函数包括main,也包括回调函数。
  • '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                         \
/path/to/crt0-efi-x86_64.o     \
-nostdlib                      \
-znocombreloc                  \
-T /path/to/elf_x86_64_efi.lds \
-shared                        \
-Bsymbolic                     \
-L /path/to/libs               \
-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.so                 \
main.efi

此过程的结果是44 kB PE可执行文件main.efi。 在实际项目中,你可能需要使用make或其他构建工具,并且可能需要构建交叉编译器。 GNU-EFI的工作方式有点绕圈子:需要将普通编译器生成的ELF文件再包装到PE中。

使用QEMU和OVMF进行仿真

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

建议使用OVMF(对于QEMU 1.6或更新版本)的方法是使用pflash参数。 下面的说明假设你将OVMF映像拆分为单独的代码和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中。

OVMF shell.png

你可以在此处找到shell命令的列表或者你可以在shell中键入帮助

创建磁盘映像

正文: 可启动磁盘

要启动UEFI应用程序,你需要创建磁盘映像并将其呈现给QEMU。 UEFI固件期望UEFI应用程序存储在GPT分区磁盘上的FAT12、FAT16或FAT32文件系统(称为EFI System Partition)中。 许多固件只支持FAT32,所以你最好使用FAT32。 根据你的平台,有几种不同的方法来创建包含UEFI应用程序的磁盘映像,但它们都是从创建零磁盘映像文件开始的。 FAT32分区的最小大小为33548800字节,此外,主GPT表和辅助GPT表还需要一些空间,再加上一些空闲空间,以便正确对齐分区。 在这些示例中,我们将创建一个48000000字节(93750512字节扇区,或48MB)的磁盘映像。

$ dd if=/dev/zero of=/path/to/uefi.img bs=512 count=93750

uefi-run助手应用程序

uefi-run应用程序对于快速测试非常有用。 它创建一个包含EFI应用程序的临时FAT映像,并启动QEMU。

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

uefi-run当前还未被任何发行版打包。 你可以使用cargo (Rust软件包管理器) 进行安装 ('cargo install uefi-run')。

Linux,需要root权限

这种方法需要根权限,并使用gdisklosetupmkdosfs。 首先,使用gdisk创建具有单个EFI System Partition的GPT分区表。

$ gdisk /path/to/uefi.img
GPT fdisk (gdisk) version 0.8.10

Partition table scan:
MBR: not present
BSD: not present
APM: not present
GPT: not present

Creating new GPT entries.

Command (? for help): o
This option deletes all partitions and creates a new protective MBR.
Proceed? (Y/N): y

Command (? for help): n
Partition number (1-128, default 1): 1
First sector (34-93716, default = 2048) or {+-}size{KMGTP}: 2048
Last sector (2048-93716, default = 93716) or {+-}size{KMGTP}: 93716
Current type is 'Linux filesystem'
Hex code or GUID (L to show codes, Enter = 8300): ef00
Changed type of partition to 'EFI System'

Command (? for help): w

Final checks complete. About to write GPT data. THIS WILL OVERWRITE EXISTING
PARTITIONS!!

Do you want to proceed? (Y/N): y
OK; writing new GUID partition table (GPT) to uefi.img.
Warning: The kernel is still using the old partition table.
The new table will be used at the next reboot.
The operation has completed successfully.

现在,你可以在磁盘映像上使用 GUID分区表,并且在扇区2048处开始使用未格式化的EFI分区。 除非你修改上面显示的命令,否则磁盘映像将使用512字节的扇区,因此EFI分区从字节1048576开始,长度为46934528字节。 使用losetup在loopback设备上向Linux显示分区。

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

(如果/dev/loop0已在使用中,则需要选择其他loopback设备。)

用mkdosfs格式化FAT32的分区。

mkdosfs -F 32 /dev/loop0

现在可以挂载分区,这样我们就可以向它复制文件。 在本例中,我们使用'/mnt'目录,但你也可以创建一个本地目录以供临时使用。

mount /dev/loop0 /mnt

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

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

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

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

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

Linux,不需要root权限

此方法使用parted, mformat, 和 mcopy ,并且可以使用用户权限执行。 首先,使用parted创建主GPT头和辅助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 if=/tmp/part.img of=/path/to/uefi.img bs=512 count=91669 seek=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表和辅助GPT表。

$ gpart create -s GPT md0
md0 created

现在我们可以向磁盘添加一个分区。 我们将指定一个'EFI'分区,这只是意味着GPT会将该分区的GUID设置为特殊的“EFI”类型。 并不是所有的bios都需要这个,分区仍然可以在Linux、FreeBSD和Windows上正常安装和浏览。

$ gpart add -t efi md0
md0p1 added

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

$ newfs_msdos -F 16 md0p1
newfs_msdos: trim 2 sectors to adjust to a multiple of 9
/dev/md2p1: 93552 sectors in 11694 FAT16 clusters (4096 bytes/cluster)
BytesPerSec=512 SecPerClust=8 ResSectors=1 FATs=2 RootDirEnts=512 Media=0xf0 FATsecs=46 SecPerTrack=9 Heads=16 HiddenSecs=0 HugeSectors=93681

现在可以挂载分区,这样我们就可以将文件复制到它。 在本例中,我们使用/mnt目录,但你也可以创建一个本地目录以供临时使用。

$ mount_msdosfs /dev/md0p1 /mnt

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

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

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

$ umount /mnt
$ mdconfig -d -u md0

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

Mac OS,不需要root权限

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:'切换到文件系统来浏览文件系统,如下所示。

OVMF browse fs.png

你可以通过输入它的名称来启动UEFI应用程序。

OVMF run app.png

请注意,一旦应用程序终止,UEFI shell将恢复。 当然,如果这是一个正确的引导程序,它将永远不会恢复,而是启动一个操作系统。

一些商用UEFI固件同时提供UEFI shell或启动用户选择的UEFI应用程序的功能,如HP EliteBook系列笔记本电脑附带的固件。 但是,大多数情况下不会向最终用户公开此功能。

调试

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

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

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

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

在真正的硬件上运行

NVRAM variables

正文: UEFI NVRAM

UEFI固件将通过文本或图形配置菜单显示其大部分配置选项,就像传统BIOS一样。 在这些菜单中进行的选择会在重新启动之间存储在NVRAM芯片中。 然而,与传统BIOS不同的是,固件开发人员可以选择通过启动时固件驻留在RAM中的方便功能向操作系统和最终用户公开部分或全部这些'NVRAM变量'。

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

可引导的UEFI应用程序

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

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

功能公开

真正的PC机向用户提供的UEFI功能数量各不相同。 例如,即使是3级机器也可能在其BIOS配置中不提供UEFI shell。 此外,一些BIOS供应商使其UEFI固件配置屏幕看起来与传统BIOS配置屏幕相同。 2类机器可能会出现一些混乱的启动菜单和配置选项。 例如,有的笔记本电脑制造商包括一个名为'OS:Windows 8'的配置选项,用于启用/禁用UEFI(即UEFI和CSM行为之间的切换)。 有的笔记本电脑在选定的引导设备上找不到可引导的UEFI应用程序(或者如果该应用程序返回的状态不是EFI_SUCCESS),它将回退到CSM行为,然后反馈驱动器的MBR已损坏。 随着时间的推移,随着第3类机器的出现,UEFI引导行为的清晰度将会提高。

为了使在真实硬件上的测试更容易,你可以将可引导UEFI应用程序安装到提供引导菜单的系统的内部硬盘驱动器上,如rEFInd。 这对于多引导场景也很方便。

PC固件开发者

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

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

苹果系统

苹果系统实现EFI 1.0,与UEFI不同,UEFI应用程序是从HFS+文件系统而不是FAT12/16/32加载的。 此外,这些UEFI应用程序必须"blessed"(直接或通过驻留在blessed目录中) 才能加载。 Blessing在HFS+文件系统中设置标志,由苹果的固件在加载应用程序之前对其进行检查。 开源的hfsutils包支持HFS文件系统中的blessing文件,但不支持目录或HFS+。

UEFI应用详情

二进制格式

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

UEFI映像的类型
类型 描述 子系统
Applications 操作系统加载程序等实用程序。 10
Boot service driver 固件在引导时使用的驱动程序 (例如磁盘驱动程序、网络驱动程序)。 11
Runtime driver 即使在操作系统加载并退出引导服务后,仍可能保持加载状态的驱动程序。 12

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

Types of machines
Name / arch Value
x86 0x014c
x86_64 0x8664
Itanium x64 0x0200
UEFI Byte Code 0x0EBC
ARM1 0x01C2
AArch (ARM x64) 0xAA64
RISC-V x32 0x5032
RISC-V x64 0x5064
RISC-V x128 0x5128

[1] 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函数
  • Microsoft's 64-bit calling convention 对于x86-64 UEFI函数
  • SMC 对于ARM UEFI函数

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

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

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

POSIX-UEFI、GNU-EFI和GCC

正文: GNU-EFI

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

对于同样使用GCC的 POSIX-UEFI,你的入口点看起来像标准的main(),不需要特殊的ABI。 此外,构建环境会为你处理编译器标志,因此无论你使用的是主机gcc还是交叉编译器,都可以在不使用'UEFI_call_wrapper'的情况下简单地调用UEFI函数。

为了方便开发人员,POSIX-UEFI和GNU-EFI都提供了'EFIAPI'宏,当面向x86时扩展为'cdecl',面向x86-64时扩展为'__ATTRIBUTE__(Ms_Abi)'。 此外,'uefi_call_wrapper' thunk将简单地在x86上传递调用。这允许相同的源代码以x86和x86-64为目标。 例如,以下主函数将在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");
  return status;
}

语言绑定

UEFI应用程序通常是用C编写的,尽管可以用可编译为机器码的任何其他语言编写。 组装也是一种选择; uefi.inc 可用于 FASM,允许将UEFI应用程序编写如下。

format pe64 dll efi
entry main

section '.text' code executable readable

include 'uefi.inc'

main:
; initialize UEFI library
InitializeLib
jc @f

    ; call uefi function to print to screen
    uefi_call_wrapper ConOut, OutputString, ConOut, _hello

@@: mov eax, EFI_SUCCESS
retn

section '.data' data readable writeable

_hello                                  du 'Hello World',13,10,0

section '.reloc' fixups data discardable

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

EFI字节码

UEFI还包括一个基于称为EFI字节码(EBC)的字节码格式的虚拟机规范,该规范可用于编写独立于平台的设备驱动程序,但不能用于UEFI应用程序。 截至2015年,EBC的使用有限。

常见问题

我的UEFI应用程序在大约5分钟后会挂起/重置

当固件将控制权交给UEFI应用程序时,它会设置一个5分钟的看门狗计时器,狗超时后固件会重新调用,因为它认定此时应用程序已挂起。 在这种情况下,固件通常会尝试重置系统(尽管VirtualBox中的OVMF固件只会导致屏幕变黑并挂起)。 为了解决这个问题,你需要在看门狗计时器超时之前刷新它。 或者,你可以使用如下代码完全禁用它

SystemTable->BootServices->SetWatchdogTimer(0, 0, 0, NULL);

显然,这对于大多数引导加载程序来说不是问题,但是如果你有一个等待用户输入的交互式加载程序,则可能会导致问题。 还请注意,如果退出固件,需要禁用看门狗定时器。

如果使用用户定义的EFI_MEMORY_TYPE值,我的引导加载程序将挂起

对于EFI中的内存管理功能,操作系统应能够出于自身目的使用0x8000000以上的'内存类型'值。 在OVFM EFI固件版本“r11337”(用于Qemu等)中,存在一个错误,固件假定内存类型在为EFI自身使用定义的值范围内,并将内存类型用作阵列索引。 最终的结果是'数组索引超出范围'错误;较高的内存类型值(例如,0x8000000以上的完全合法值)导致64位版本的固件崩溃(页面错误),并导致32位版本的固件报告不正确的'属性'值。 同样的错误也存在于任何版本的VirtualBoxEFI固件中(看起来像旧版本的OVFM); 我怀疑(但不知道)该漏洞可能存在于从tianocore项目(不仅仅是OVFM)衍生的各种固件中。

另见

文章

外部链接

de:Unified_Extensible_Firmware_Interface