“pull oneself up by one’s bootstraps.”

拽着鞋带把自己拉起来

大家在安装 Arch Linux 或者其他 Linux 发行版时,可能会看到很多有关启动或者引导的名词,例如 BIOS 、UEFI 、GRUB 、ESP 、GPT 、LBA 、MBR 等等。有些名词比较熟悉,有些就会一头雾水,今天就来讲讲这些名词。

BIOS vs UEFI

BIOS(Basic Input Output System,基本输入输出系统)是固化在计算机主板中的程序代码,其主要功能是在计算机上电时对硬件进行初始化配置,并将硬件操作封装为BIOS中断服务。这样,各种硬件间的差异便由 BIOS 负责维护,程序直接调用 BIOS 中断服务即可实现对硬件的控制。

  • 加电自检程序,在开机时负责检测硬件设备是否正常工作
  • 系统初始化程序,其中包括硬件设备的初始化以及创建 BIOS 中断向量等
  • 适配外围即插即用设备
  • CMOS 设置程序,负责读写保存在 CMOS 中的系统设置信息

UEFI(Unified Extensible Firmware Interface,统一可扩展固件接口)则是一种规范,它描述了操作系统和平台固件之间的接口,其目的是为操作系统和平台固件定义一种通信方法。在 UEFI 中,设备的访问是通过句柄(Handle)和协议(Protocol)抽象出来的。UEFI 通过将基础实现隔离在规范之外,以避免给设备的访问者带来负担,进而促进现有 BIOS 代码的重用。

uefi 组成结构

BIOS ,启动!

在系统上电后,CPU 运行于实模式工作环境中,数据位宽为 16 位,最大物理地址寻址范围是 0~1 MB,其中的物理地址 0x0C0000~0x0FFFFF 保留给 BIOS 使用。开机后, CPU 首先跳转到物理地址 0x0FFFF0 处执行程序。一般情况下,这里是一条跳转指令, CPU 通过执行此处的跳转指令跳转到真正的 BIOS 入口地址处执行:

  1. BIOS代码首先做的是 POST(Power On Self Test,加电自检)操作,主要是检测关键设备是否正常工作,设备设置是否与CMOS中的设置一致。如果发现硬件错误,则通过喇叭报警
  2. 初始化显示设备并显示显卡信息,接着初始化其他设备
  3. 检测CPU和内存并显示检测结果
  4. 检测标准设备,例如硬盘、光驱、串口设备、并口设备等
  5. 检测即插即用设备,并为这些设备分配中断号、 I/O 端口和 DMA 通道等资源
  6. 如果硬件配置发生变化,那么这些变化的配置将更新到 CMOS 中
  7. 根据配置的启动顺序引导设备启动,通过BIOS中断将设备的引导程序入内存
  8. 将处理器的控制权交给引导程序,最终引导进入操作系统

UEFI ,启动!

uefi 启动流程

  1. 验证阶段(Security,SEC)。系统上电后,CPU 开始执行第一条指令,此时系统就进入 SEC 阶段。这个阶段的内存尚未被初始化,不可使用。所以, SEC 阶段最主要的工作是建立一些临时内存并将 CPU 切换到保护模式,这里提到的临时内存可以是处理器的缓存,亦或者系统的物理内存。
  2. EFI 环境预初始化阶段(Pre-EFI Initialization Environment,PEI)。PEI 阶段最主要的工作就是对内存、CPU 以及芯片组等关键设备进行初始化。由于这部分代码没有进行压缩,因此代码必须越精简越好。而且,在PEI阶段还要确定操作系统的引导路径,初始化 UEFI 驱动和固件需要的内存。
  3. 驱动运行环境阶段(Driver Execution Environment,DXE)。DXE 是 EFI 最重要的阶段,大部分的驱动、固件加载工作都是在这个阶段完成的。
  4. 引导设备选择阶段(Boot Device Select,BDS)。BDS 阶段的主要工作是初始化控制台设备的环境变量,尝试加载环境变量列表中记录的驱动,并尝试从环境变量列表中记录的启动设备中启动。
  5. 临时系统运行阶段(Transient System Load,TSL)。这个阶段将进入 UEFI 的临时 Shell 系统环境。
  6. 运行时阶段(RunTime,RT)。当操作系统调用 EFI_BOOT_SERVICES.ExitBootServices 服务后,系统进入 RT 阶段。此时,DXE 与引导服务都将销毁,只有 EFI 运行时服务和 EFI 系统表可以继续使用。
  7. 后世阶段(After Life,AL)。当操作系统调用 EFI_RUNTIME_SERVICES.ResetSystem 服务或者调用 ACPI SleepState,系统进入 AL 阶段。触发异步事件(比如:SMI、NMI)亦可使系统进入 AL 阶段,这在服务器和工作站中比较常见。

BIOS 的缺点 & UEFI 的优点

  • 正如 systemd 取代 init.d 的原因之一,BIOS 不支持异步工作模式,只能使用中断来实现各种服务;而 UEFI 可以舍弃中断这种比较耗时的操作外部设备的方式,仅仅保留了时钟中断。外部设备的操作采用“事件+异步操作”完成。
  • 功能拓展性与拓展性:BIOS 代码采用静态链接,增加硬件功能时,必须将 16 位代码放置在 0x0C0000~0x0DFFFF 区间,初始化时将其设置为约定的中断处理程序。而且 BIOS 没有提供动态加载设备驱动的方案。而 UEFI 系统的可扩展性体现在两个方面:一是驱动的模块化设计;二是软硬件升级的兼容性。大部分硬件的初始化通过 UEFI 驱动实现。每个驱动是一个独立的模块,可以包含在固件中,也可以放在设备上,运行时根据需要动态加载。UEFI 中的每个表和协议(包括驱动)都有版本号,这使得系统升级过程更加简单、平滑。
  • BIOS 不支持从硬盘 2TB 以上的地址空间引导:受限于 BIOS 硬盘的寻址方式,BIOS 硬盘采用 32 位地址,因而引导扇区的最大逻辑块地址是 2^32(换算成字节地址,即 2^32×512B=2TB)

MBR vs GPT

Legacy MBR(Master Boot Record)是为 BIOS 引导方式设计的磁盘引导扇区结构,其绝大部分功能结构都保存在磁盘的引导扇区。而 GPT(GUID Partition table)却是区别于 legacy MBR 磁盘布局的一种全新布局,它专为 UEFI 固件使用:

  1. 64 位的LBA(Logical Block Address)磁盘扇区寻址,而不是 32 位
  2. 支持更多分区,而不仅仅是四个主分区
  3. 为主分区表提供冗余的备份分区表
  4. 使用版本号和大小字段为将来做扩展
  5. 使用 CRC32 字段改进数据完整性
  6. 定义用于唯一标识每个分区的 GUID
  7. 使用 GUID 和属性定义分区类型
  8. 每个分区包含一个 36 字符的人类可读名称

为了兼容 legacy MBR 磁盘布局,GPT 仍使用 legacy MBR 的引导扇区结构,但 UEFI 固件不会执行 MBR 上的启动代码。GPT 为了与 legacy MBR 的引导扇区结构相区分特将引导扇区结构命名为 Protective MBR(PMBR),并使用 GPT 伪分区模拟 MBR 分区表。

GPT 的 Protective MBR 引导扇区结构只使用一个分区记录表项,这个分区将占用 Protective MBR 之后的整个磁盘空间,剩余三个表项保留使用并填充 0 。

pmbr

可以看到备份分区表的顺序是相反的,这也非常符合我们对这种记录结构的直觉

GUID

描述 GUID值
未使用 00000000-0000-0000-0000-000000000000
EFI System Partition C12A7328-F81F-11D2-BA4B-00A0C93EC93B
Partition Containing a legacy MBR 024DEE41-33E7-11D3-9D69-0008C781F39F
FAT12/16/32/NTFS EBD0A0A2-B9E5-4433-87C0-68B6B72699C7
EXT4 0FC63DAF-8483-4772-8E79-3D69D8477DE4

区别于分区的 UUID 。这个 GUID 用于标识分区的内容,类似于MBR中的 OSType 字段。每个文件系统都必须发布其唯一的 GUID 。

ESP & FAT & PE

UEFI 的 ESP 文件系统是指 UEFI 系统分区(EFI System Partition)上的文件系统,它是一个使用 FAT32 格式化的小分区,通常为 100MB,其中存储了已安装系统的 UEFI 引导加载程序以及启动时固件使用的应用程序。UEFI 固件在启动时会加载 ESP 分区上的 .efi 文件,开始加载操作系统。

UEFI 不能运行 ELF 格式的可执行程序,只能运行 PE 格式的。使用 objcopy 处理生成的二进制文件,将其格式改变为 PE+ ,或者使用交叉编译的方式,将 target 设置为 x86_64-unknown-uefiaarch64-unknown-uefi

# file /boot/EFI/GRUB/grubx64.efi
/boot/EFI/GRUB/grubx64.efi: PE32+ executable (EFI application) x86-64 (stripped to external PDB), for MS Windows, 4 sections

Linux ,启动!

不管接口或者规范如何变化,计算机的启动一般都会是三个步骤:

  1. CAR(Cache As RAM) ,这个阶段连内存都用不了
  2. 能执行汇编语言
  3. 设置堆栈环境,这时能执行 C 等高级语言

这里主要介绍在 BootLoader 阶段完成的工作。

GRUB ,启动!

grub 是一款不断扩充功能直到最后历史包袱积重难返的软件,因此它的启动过程比较复杂。

  1. Stage 1:执行GRUB主程序。第一阶段是用来执行 GRUB 主程序的,这个主程序必须放在启动区中(也就是 MBR 或者引导扇区中)。但是 MBR 太小了,所以只能安装 GRUB 的最小的主程序,而不能安装 GRUB 的相关配置文件。这个主程序主要是用来启动 Stage 1.5 和 Stage 2 的。

  2. Stage 1.5:识别不同的文件系统。Stage 2 比较大,只能放在文件系统中(分区),但是 Stage 1 不能识别不同的文件系统,所以不能直接加载 Stage 2。这时需要先加载 Stage 1.5,由 Stage 1.5 来加载不同文件系统中的 Stage 2。

还有一个问题,难道 Stage 1.5 不是放在文件系统中的吗?如果是,那么 Stage 1 同样不能找到 Stage 1.5。其实,Stage 1.5 还真没有放在文件系统中,而是在安装 GRUB 时,直接安装到紧跟 MBR 之后的 32KB 的空间中,这段硬盘空间是空白无用的,而且是没有文件系统的,所以 Stage 1 可以直接读取 Stage 1.5。读取了 Stage 1.5 就能识别不同的文件系统,才能加载 Stage 2。

  1. Stage 2:加载GRUB的配置文件。Stage 2 阶段主要就是加载 GRUB 的配置文件 /boot/grub/grub.conf,然后根据配置文件中的定义,加载内核和虚拟文件系统。接下来内核就可以接管启动过程,继续自检与加载硬件模块了。

GRUB2 ,启动!

grub2 是 grub 的重写版本,启动过程更简单明了。grub2 将 boot.img 转换后的内容安装到 MBR(VBR或EBR) 中的 boot loader 部分,将 diskboot.img 和 kernel.img 结合成为 core.img,同时还会嵌入一些模块或加载模块的代码到 core.img 中,然后将 core.img 转换后的内容安装到磁盘的指定位置处。

grub2 的 core 阶段相当于 grub 的 stage1.5 和 stage2 的结合,它可以直接加载不同的文件系统和模块,而不需要额外的驱动程序。

core 阶段通常存储在一个专门的 UEFI 分区或者 /boot/grub 目录中。

  1. Stage 1:第一阶段为执行 boot loader 的主程序,这个主程序必须要被安装在引导区,也就是是 MBR 或者启动扇区。但因为 MBR 空间有限,所以,MBR 或启动扇区通常仅安装 boot loader 的最小主程序,并没有安装 loader 的相关配置文件。

  2. Stage 2:第二阶段通过 boot loader 加载所有配置文件与相关的环境参数文件(包括文件系统定义与主要配置文件 grub.cfg),通常配置文件都在 /boot 目录下。

# ls /boot
EFI  grub  initramfs-linux-fallback.img  initramfs-linux.img  vmlinuz-linux
# file vmlinuz-linux
vmlinuz-linux: Linux kernel x86 boot executable bzImage, version 6.4.12-arch1-1 (linux@archlinux) #1 SMP PREEMPT_DYNAMIC Thu, 24 Aug 2023 00:38:14 +0000, RO-rootFS, swap_dev 0XC, Normal VGA
# tree EFI
EFI
└── GRUB
    └── grubx64.efi

2 directories, 1 file
# tree grub
grub
├── fonts
│   └── unicode.pf2
├── grub.cfg
├── grubenv
├── locale
│   ├── ast.mo
│   ├── ca.mo
│   ├── da.mo
│   ├── de_CH.mo
│   ├── de@hebrew.mo
......

.mo 文件都是供 grub 加载的模块,包含了对字体、图像、USB 设备等。而 initramfs-linux.img 是一个初始内存文件系统,它是一个压缩的归档文件,包含了一些必要的程序和驱动,用于在启动过程中加载根文件系统。vmlinuz-linux 是一个 Linux 内核镜像,它是一个可执行的二进制文件,用于在 UEFI 启动模式下被固件直接加载。这两个文件通常位于 /boot 目录下,是 Linux 系统启动的重要组成部分。

initramfs 的目的在于提供启动过程中所需要的最重要内核模块,以让系统启动过程可以顺利完成。需要 initramfs 的原因,是因为内核模块放置于 /lib/modules/$(uname -r)/kernel/ 当中, 这些模块必须要根目录被挂载时才能够被读取。但是如果内核本身不具备磁盘的驱动程序时, 就无法挂载根目录,也就没有办法获取到驱动程序,造成无法启动

initramfs 可以将 /lib/modules/ 内的启动过程中必需的模块打包到 initramfs 中,然后在启动时,通过主机的 INT 13 硬件功能将该文件读出来解压缩,并且 initramfs 在内存内会模拟成为根目录, 由于此虚拟文件系统主要包含磁盘与文件系统的模块,因此内核最后就能够识别实际的磁盘,然后就能够进行实际根目录的挂载,因此,initramfs 内所包含的模块大多是与启动过程有关,且主要以文件系统及硬盘模块 (如 usb, SCSI 等) 为主的

通常在以下情况下需要 initramfs:

  • 根目录所在磁盘为 SATA、USB 或 SCSI 等接口
  • 根目录所在文件系统为 LVM, RAID 等特殊格式
  • 根目录所在文件系统为非传统 Linux 识别的文件系统
  • 其他必须要在内核加载时提供的模块

在发行版更新内核时,一般都会执行一个钩子函数,重新生成这些重要部件:

:: Running post-transaction hooks...
(1/7) Reloading system manager configuration...
(2/7) Reloading device manager configuration...
(3/7) Arming ConditionNeedsUpdate...
(4/7) Updating module dependencies...
(5/7) Install DKMS modules
==> dkms install --no-depmod v4l2loopback/0.12.7 -k 6.5.2-arch1-1
==> depmod 6.5.2-arch1-1
(6/7) Updating linux initcpios...
==> Building image from preset: /etc/mkinitcpio.d/linux.preset: 'default'
==> Using default configuration file: '/etc/mkinitcpio.conf'
  -> -k /boot/vmlinuz-linux -g /boot/initramfs-linux.img --microcode /boot/*-ucode.img
==> Starting build: '6.5.2-arch1-1'
  -> Running build hook: [base]
  -> Running build hook: [udev]
  -> Running build hook: [autodetect]
  -> Running build hook: [modconf]
  -> Running build hook: [kms]
  -> Running build hook: [keyboard]
==> WARNING: Possibly missing firmware for module: 'xhci_pci'
  -> Running build hook: [keymap]
  -> Running build hook: [consolefont]
==> WARNING: consolefont: no font found in configuration
  -> Running build hook: [block]
  -> Running build hook: [filesystems]
  -> Running build hook: [fsck]
==> Generating module dependencies
==> Creating zstd-compressed initcpio image: '/boot/initramfs-linux.img'
==> Image generation successful
==> Building image from preset: /etc/mkinitcpio.d/linux.preset: 'fallback'
==> Using default configuration file: '/etc/mkinitcpio.conf'
  -> -k /boot/vmlinuz-linux -g /boot/initramfs-linux-fallback.img -S autodetect --microcode /boot/*-ucode.img
==> Starting build: '6.5.2-arch1-1'
  -> Running build hook: [base]
  -> Running build hook: [udev]
  -> Running build hook: [modconf]
  -> Running build hook: [kms]
==> WARNING: Possibly missing firmware for module: 'ast'
  -> Running build hook: [keyboard]
==> WARNING: Possibly missing firmware for module: 'xhci_pci'
  -> Running build hook: [keymap]
  -> Running build hook: [consolefont]
==> WARNING: consolefont: no font found in configuration
  -> Running build hook: [block]
==> WARNING: Possibly missing firmware for module: 'aic94xx'
==> WARNING: Possibly missing firmware for module: 'bfa'
==> WARNING: Possibly missing firmware for module: 'qed'
==> WARNING: Possibly missing firmware for module: 'qla1280'
==> WARNING: Possibly missing firmware for module: 'qla2xxx'
==> WARNING: Possibly missing firmware for module: 'wd719x'
  -> Running build hook: [filesystems]
  -> Running build hook: [fsck]
==> Generating module dependencies
==> Creating zstd-compressed initcpio image: '/boot/initramfs-linux-fallback.img'
==> Image generation successful
(7/7) Reloading system bus configuration...
total 0h 5m 21s

参考资料