前言

我原本使用 GRUB 作为 Bootloader,但 GRUB 让我感觉不太舒服。UEFI 固件可以直接引导可执行 EFI 文件,并不一定需要中间的 Bootloader 来引导操作系统。此外,GRUB 总是会在启动前显示引导选项菜单,而我并没有心思去美化这个菜单。

让 UEFI 固件不经过中间 Bootloader 引导 Linux 似乎是可行的,因此我决定不再使用 GRUB,而是选择让 UEFI 固件直接引导 Linux 内核映像。

不同的引导方式

通过 GRUB 引导

在 BIOS 平台上,如果使用主引导记录 (MBR) 分区表,在创建分区时往往会在 MBR 与第一个分区之间留出 1~2MB 的空间,这被称之为 MBR 后间隙。在安装时,安装程序会将 GRUB 嵌入到 MBR 后间隙中,并重写 MBR,使 MBR 中的引导代码可以启动 MBR 后间隙中的 GRUB。在启动时,BIOS 会先执行 MBR 中的引导代码,随后这段引导代码将会加载并启动 GRUB。

如果使用 GUID 分区表 (GPT),GRUB 则需要被安装到指定的 BIOS 启动分区中。在启动时 BIOS 将会从 BIOS 启动分区加载并启动 GRUB。

在 UEFI 平台上,GRUB 呈现为一个可执行 EFI 文件。GRUB 通常会被安装到 EFI 系统分区中,作为一个可执行 EFI 文件存在。在安装时,安装程序也会向 UEFI 引导序列中加入一个 GRUB 的引导选项。在 UEFI 固件中选择启动 GRUB 后,UEFI 固件将会从 EFI 系统分区加载并启动 GRUB。

在 GRUB 启动后,GRUB 会根据其配置文件呈现启动项。GRUB 可以读取 ext4 和 BtrFS 分区,因此可以将 Linux 内核与 initramfs 存储在 Linux 根分区中(往往会存储在 /boot 中)。当选择启动 Linux 时,GRUB 将会将 Linux 内核 (vmlinux/vmlinuz) 与 initramfs 加载到内存中,然后启动 Linux。

EFISTUB 引导

在编译 Linux 内核时设置 CONFIG_EFI_STUB=y 即可启用 EFISTUB 引导,Arch Linux 的内核默认启用了 EFISTUB 引导。启用了 EFISTUB 引导的 Linux 内核映像包含一段被称之为 UEFI Boot Stub 的代码,它会在 Linux 内核映像被 UEFI 固件引导后首先执行。UEFI Boot Stub 会对已加载的内核映像进行修改,以确保 Linux 内核能够被启动,然后启动 Linux 内核。

UEFI Boot Stub 所做的工作类似于 Bootloader,所以在某种意义上它就是一个 Bootloader。

UEFI 引导序列可以使用 efibootmgr 等工具修改。添加了 Linux 内核映像的引导选项后,在 UEFI 固件中选择启动 Linux 时,UEFI 固件将会引导 Linux 内核映像。

统一内核映像 (UKI)

Linux 内核和 Linux 引导过程中需要用到的资源可以被制作成一个可以被 UEFI 固件直接执行的可执行 EFI 文件,这个 EFI 文件就是一个统一内核映像 (Unified Kernel Image),简称 UKI。一个 UKI 可以包含:

相比于 EFISTUB,UKI 可以整合 initramfs 和 CPU 微码等在引导过程中会使用到的资源。这使得 UKI 相比于支持 EFISTUB 引导的 Linux 内核映像集成度更高,也更容易使用,不需要太多配置即可被 UEFI 固件引导。

UKI 的结构

UKI 是一个可执行 EFI 文件,而 EFI 文件也是一个 PE/COFF 文件。以我的系统上正在使用的 UKI 为例,其 PE 区段列表如下:

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00009930  0000000000003000  0000000000003000  00000400  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .reloc        0000000c  000000000000d000  000000000000d000  00009e00  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .data         00002008  000000000000e000  000000000000e000  0000a000  2**4
                  CONTENTS, ALLOC, LOAD, DATA
  3 .dynamic      00000100  0000000000011000  0000000000011000  0000c200  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  4 .rela         000004f8  0000000000012000  0000000000012000  0000c400  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .dynsym       00000018  0000000000013000  0000000000013000  0000ca00  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .sbat         000000ea  0000000000015000  0000000000015000  0000cc00  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  7 .sdmagic      00000030  0000000000015100  0000000000015100  0000ce00  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  8 .osrel        0000017b  0000000000015200  0000000000015200  0000d000  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  9 .uname        0000000c  0000000000015400  0000000000015400  0000d200  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 10 .splash       0005c572  0000000000015600  0000000000015600  0000d400  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 11 .linux        00ab0b18  0000000000071c00  0000000000071c00  00069a00  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
 12 .initrd       0638eac7  0000000000b22800  0000000000b22800  00b1a600  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA

其中:

  • .linux 包含 Linux 内核映像
  • .initrd 包含 initramfs 和 CPU 微码
  • .splash 包含启动屏幕图片

还有一些在我的 UKI 中未使用的区段,例如 .cmdline,其包含嵌入的内核参数。

systemd-stub 与 UKI 创建

与 EFISTUB 引导中的 UEFI Boot Stub 一样,systemd-stub 也实现了类似的功能——即在 UKI 被引导后首先执行,加载 Linux 内核与相关资源,然后启动 Linux 内核。systemd-stub 提供了仅包含 UEFI Boot Stub 部分的可执行 EFI 文件,它们可以作为空白模板使用。

在 UKI 被引导后,systemd-stub 会从 UKI 中加载 Linux 内核和所需的资源,如在 .linux 区段中寻找 ELF 格式的 Linux 内核、从 .initrd 区段加载 initramfs 和 CPU 微码、从 .cmdline 区段加载嵌入的内核参数等。所以可以通过向空白模板中加入更多的 PE 区段来制作一个 UKI

mkinitcpiodracut 等工具不仅可以用于创建 initramfs,也可以用于创建 UKI。下面将会介绍使用 mkinitcpio 创建 UKI 的操作。

使用 mkinitcpio 创建 UKI

在创建 UKI 之前,需要先设置内核参数并修改 mkinitcpio 的预设配置。

嵌入内核参数

首先需要在 UKI 中嵌入你想要使用的内核参数,创建 /etc/kernel/cmdline 并写入内核参数。下面是一个例子:

root=UUID=b9fb5b31-07f1-408c-9447-10a1b2476b4d rw splash loglevel=3

在创建 initramfs 时,mkinitcpio 会读取其中的内容并将其作为内核参数将其嵌入 UKI 中。

注意
你也可以选择在 UEFI 引导选项中添加想要使用的内核参数。但请注意,如果启用了 Secure Boot 且 UKI 中嵌入了内核参数(即存在 .cmdline 区段),内核将会只使用嵌入的内核参数,并忽略从外部传递的额外参数。详见 systemd-stub

修改 mkinitcpio 预设

mkinitcpio 可以创建 UKI,但在此之前需要对 mkinitcpio 的预设进行一些修改。修改 /etc/mkinitcpio.d/linux.preset

  • 取消对 PRESET_uki=... 的注释来启用 UKI 创建,并指定 UKI 的保存位置
  • 注释 PRESET_image 来关闭 initramfs 映像创建

UKI 通常应该被保存到 EFI 系统分区中。以使用 linux 内核的 Arch Linux、EFI 系统分区挂载到 /efi 为例,修改完成后的 linux.preset 看起来应该像这样:

ALL_config="/etc/mkinitcpio.conf"
ALL_kver="/boot/vmlinuz-linux"
ALL_microcode=(/boot/*-ucode.img)

PRESETS=('default' 'fallback')

#default_config="/etc/mkinitcpio.conf"
#default_image="/boot/initramfs-linux.img"
default_uki="/efi/EFI/Linux/arch-linux.efi"
#default_options="--splash /usr/share/systemd/bootctl/splash-arch.bmp"

#fallback_config="/etc/mkinitcpio.conf"
#fallback_image="/boot/initramfs-linux-fallback.img"
fallback_uki="/efi/EFI/Linux/arch-linux-fallback.efi"
fallback_options="-S autodetect"
提示
  • 如果要将启动屏幕图片整合进 UKI 当中,只需要取消对 <PRESET_NAME>_options 的注释并在其中添加 --splash /path/to/your/splash_picture.bmp 即可。
  • 如果需要为某一预设单独设置内核参数,在 <PRESET_NAME>_options 中添加 --cmdline /path/to/your/cmdline 即可。
  • 如果选择不使用嵌入的内核参数,在 <PRESET_NAME>_options 中添加 --no-cmdline 即可。

接下来就只需要使用 mkinitcpio 来创建 UKI 了。

$ mkdir -p /efi/EFI/Linux
$ mkinitcpio -p linux

引导 UKI

警告
由于在这里并没有对 UKI 进行签名,因此如果启动了 Secure Boot,UEFI 固件将会拒绝引导 UKI。如果要引导未签名的 UKI,请确保 Secure Boot 处于关闭状态。

使用 UEFI 引导选项

在这里以使用 efibootmgr 为例,向 UEFI 引导序列中添加一个引导选项:

$ efibootmgr --create \
    -d /dev/sdX -p Y \
    --label "Arch Linux" \
    --loader "EFI/Linux/arch-linux.efi"

其中 sdX 为 EFI 系统分区所在的设备名称,Y 为 EFI 系统分区的分区编号,--loader 指定了要引导的 UKI 在分区中的位置。

如果需要在 UEFI 引导选项中指定内核参数,使用 -u parameters 来指定以 UTF-16 编码的额外参数:

$ efibootmgr --create \
>   -d /dev/sdX -p Y \
>   --label "Arch Linux" \
>   --loader "EFI/Linux/arch-linux.efi" \
>   -u "root=UUID=b9fb5b31-07f1-408c-9447-10a1b2476b4d rw splash loglevel=3"

使用 UEFI Shell 引导

如果只是想临时地引导一个 UKI 而并不想把它加入引导序列中,又或者是忘记向加入引导序列加入引导选项而无法引导 UKI,可以进入 UEFI Shell 来手动引导 UKI。

在进入 UEFI Shell 后,屏幕上通常会显示可用的文件系统 (FS) 和块设备 (BLK)。如果没有的话,执行 map 指令可以显示可用的文件系统和块设备。它们看起来通常是类似于这样的:

Mapping table
      FS0: Alias(s):HD0b:;BLK1:
        PciRoot(0x0)/Pci(0x1,0x1)/Pci(0x0,0x0)/
        NVMe(0x1,00-11-22-33-44-55-66-77)/
        HD(1,GPT,C9CFBD7E-CF6B-4FBA-BC16-FA9BC170AE5F,0x800,0x180000)
      BLK3: Alias(s):
        PciRoot(0x0)/Pci(0x1,0x1)/Pci(0x0,0x0)/
        NVMe(0x1,00-11-22-33-44-55-66-77)/
        HD(4,GPT,11F19580-6B4A-4D1B-B963-6C434DC14700,0x2180800,0x38205800)
# ...

在可用的文件系统中找到 EFI 系统分区,用下面的指令进入 EFI 系统分区并引导 UKI:

Shell> FS0:
FS0:\> cd EFI\Linux
FS0:\EFI\Linux> arch-linux.efi root=UUID=b9fb5b31-07f1-408c-9447-10a1b2476b4d rw splash loglevel=3

在执行最后一条指令后,Linux 应该会正常启动。

关于 Secure Boot

如果要使 UKI 能够在启用了 Secure Boot 的平台上被引导,则需要对 UKI 进行签名。大多数设备使用来自微软的证书,这意味着只有被微软的密钥签名的映像才能够被引导。同时,这也意味着你必须用自己的证书替换掉设备原有的证书才能在启用了 Secure Boot 的情况下引导自己签名的 UKI。

由于配置 Secure Boot 的内容较长,因此我会在另一篇文章中介绍 Secure Boot。

结语

原本以为只是很简短的一篇关于如何创建并使用 UKI 的教程,实在没想到最后居然写了这么长。虽然说 objdump 和各种代码展示也占用了不少的篇幅,但整体来讲内容还是较为庞大的。

一开始还以为最终写出来也基本上只是照搬 Wiki,但在撰文的过程中我也在不断地学习,向文章中添加自己的理解。从不同的启动方式到 UKI 的结构、再到创建和引导 UKI,期间查阅各种资料,自己的理解也逐渐变得更深刻了。


参考资料