“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 代码的重用。
BIOS ,启动!
在系统上电后,CPU 运行于实模式工作环境中,数据位宽为 16 位,最大物理地址寻址范围是 0~1 MB,其中的物理地址 0x0C0000~0x0FFFFF 保留给 BIOS 使用。开机后, CPU 首先跳转到物理地址 0x0FFFF0 处执行程序。一般情况下,这里是一条跳转指令, CPU 通过执行此处的跳转指令跳转到真正的 BIOS 入口地址处执行:
- BIOS代码首先做的是 POST(Power On Self Test,加电自检)操作,主要是检测关键设备是否正常工作,设备设置是否与CMOS中的设置一致。如果发现硬件错误,则通过喇叭报警
- 初始化显示设备并显示显卡信息,接着初始化其他设备
- 检测CPU和内存并显示检测结果
- 检测标准设备,例如硬盘、光驱、串口设备、并口设备等
- 检测即插即用设备,并为这些设备分配中断号、 I/O 端口和 DMA 通道等资源
- 如果硬件配置发生变化,那么这些变化的配置将更新到 CMOS 中
- 根据配置的启动顺序引导设备启动,通过BIOS中断将设备的引导程序入内存
- 将处理器的控制权交给引导程序,最终引导进入操作系统
UEFI ,启动!
- 验证阶段(Security,SEC)。系统上电后,CPU 开始执行第一条指令,此时系统就进入 SEC 阶段。这个阶段的内存尚未被初始化,不可使用。所以, SEC 阶段最主要的工作是建立一些临时内存并将 CPU 切换到保护模式,这里提到的临时内存可以是处理器的缓存,亦或者系统的物理内存。
- EFI 环境预初始化阶段(Pre-EFI Initialization Environment,PEI)。PEI 阶段最主要的工作就是对内存、CPU 以及芯片组等关键设备进行初始化。由于这部分代码没有进行压缩,因此代码必须越精简越好。而且,在PEI阶段还要确定操作系统的引导路径,初始化 UEFI 驱动和固件需要的内存。
- 驱动运行环境阶段(Driver Execution Environment,DXE)。DXE 是 EFI 最重要的阶段,大部分的驱动、固件加载工作都是在这个阶段完成的。
- 引导设备选择阶段(Boot Device Select,BDS)。BDS 阶段的主要工作是初始化控制台设备的环境变量,尝试加载环境变量列表中记录的驱动,并尝试从环境变量列表中记录的启动设备中启动。
- 临时系统运行阶段(Transient System Load,TSL)。这个阶段将进入 UEFI 的临时 Shell 系统环境。
- 运行时阶段(RunTime,RT)。当操作系统调用
EFI_BOOT_SERVICES.ExitBootServices
服务后,系统进入 RT 阶段。此时,DXE 与引导服务都将销毁,只有 EFI 运行时服务和 EFI 系统表可以继续使用。 - 后世阶段(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 固件使用:
- 64 位的LBA(Logical Block Address)磁盘扇区寻址,而不是 32 位
- 支持更多分区,而不仅仅是四个主分区
- 为主分区表提供冗余的备份分区表
- 使用版本号和大小字段为将来做扩展
- 使用 CRC32 字段改进数据完整性
- 定义用于唯一标识每个分区的 GUID
- 使用 GUID 和属性定义分区类型
- 每个分区包含一个 36 字符的人类可读名称
为了兼容 legacy MBR 磁盘布局,GPT 仍使用 legacy MBR 的引导扇区结构,但 UEFI 固件不会执行 MBR 上的启动代码。GPT 为了与 legacy MBR 的引导扇区结构相区分特将引导扇区结构命名为 Protective MBR(PMBR),并使用 GPT 伪分区模拟 MBR 分区表。
GPT 的 Protective MBR 引导扇区结构只使用一个分区记录表项,这个分区将占用 Protective MBR 之后的整个磁盘空间,剩余三个表项保留使用并填充 0 。
可以看到备份分区表的顺序是相反的,这也非常符合我们对这种记录结构的直觉
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-uefi
或 aarch64-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 ,启动!
不管接口或者规范如何变化,计算机的启动一般都会是三个步骤:
- CAR(Cache As RAM) ,这个阶段连内存都用不了
- 能执行汇编语言
- 设置堆栈环境,这时能执行 C 等高级语言
这里主要介绍在 BootLoader 阶段完成的工作。
GRUB ,启动!
grub 是一款不断扩充功能直到最后历史包袱积重难返的软件,因此它的启动过程比较复杂。
-
Stage 1:执行GRUB主程序。第一阶段是用来执行 GRUB 主程序的,这个主程序必须放在启动区中(也就是 MBR 或者引导扇区中)。但是 MBR 太小了,所以只能安装 GRUB 的最小的主程序,而不能安装 GRUB 的相关配置文件。这个主程序主要是用来启动 Stage 1.5 和 Stage 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。
- 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 目录中。
-
Stage 1:第一阶段为执行 boot loader 的主程序,这个主程序必须要被安装在引导区,也就是是 MBR 或者启动扇区。但因为 MBR 空间有限,所以,MBR 或启动扇区通常仅安装 boot loader 的最小主程序,并没有安装 loader 的相关配置文件。
-
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
参考资料
- 一个 UEFi 引导程序的实现
- GRUB legacy document
- GRUB2 document
- Linux启动流程(二):grub2
- 计算机是如何启动的?从未上电到操作系统启动