Linux 内核启动流程深度剖析:从 EFI Stub 到统一内核镜像(UKI)的演进
引言:为什么需要理解启动过程
在日常运维和开发中,我们很少关注系统启动的那几秒到底发生了什么。然而当系统无法启动、initramfs 构建失败、UKI 签名验证不通过,或者需要调试 systemd-boot 配置时,对启动流程的深入理解就从"锦上添花"变成了"救命稻草"。
本文将从 UEFI 固件开始,逐步解剖 Linux 内核从加电到用户空间的完整路径:EFI Stub 入口点、内核解压缩、start_kernel() 的全局初始化、initramfs 临时根文件系统的运作机制,以及近年来兴起的 UKI(Unified Kernel Image)统一内核镜像方案——它正在重塑不可变基础设施的运维模式。
一、UEFI 启动概览
现代 x86_64 和 ARM64 服务器几乎统一到了 UEFI(Unified Extensible Firmware Interface)启动模式。UEFI 替代了传统的 BIOS,提供了以下关键能力:
- GPT 分区表支持:突破 MBR 的 2TB 和 4 主分区限制
- EFI 系统分区(ESP):FAT32 格式的独立分区,存放引导程序和内核
- Secure Boot:基于 PK/KEK/db/dbx 链的签名验证机制
- 运行时服务:
GetVariable/SetVariable等接口,操作系统可在运行时访问
UEFI 启动流程的核心步骤:
SEC (Security Phase) → PEI (Pre-EFI Initialization) → DXE (Driver Execution Environment)
→ BDS (Boot Device Selection) → TSL (Transient System Load) → RT (Runtime)
在 BDS 阶段,固件扫描 ESP 分区中的 \EFI\BOOT\BOOTX64.EFI(或其架构特定变体),加载并执行 EFI 应用程序。这个 EFI 应用程序可以是:
- GRUB2 (
grubx64.efi) → 加载配置 → 加载内核 - systemd-boot (
systemd-bootx64.efi) → 加载 UKI 或直接加载内核 - Linux EFI Stub → 内核自身即为 EFI 应用程序,无需中间引导器
二、Linux 内核作为 EFI 应用程序:EFI Stub 机制
从 Linux 3.3 开始(x86),内核内置了 EFI Stub 能力,使得内核镜像本身可以直接作为 PE/COFF 格式的 EFI 应用程序运行。这是理解 UKI 方案的基础。
内核的 PE 头部中有一个特殊的 Section:.reloc 和 .text,其中包含了 efi_main 入口点。当 UEFI 固件跳转到该入口时,内核执行以下核心逻辑:
// 简化后的 efi_main 流程(arch/x86/boot/compressed/eboot.c 和 drivers/firmware/efi/libstub/ 中)
efi_status_t efi_main(efi_handle_t handle, efi_system_table_t *sys_table)
{
// 1. 初始化 EFI 引导服务:获取内存映射、配置表、图形输出协议
efi_init(handle, sys_table, &boot_params);
// 2. 解析内核命令行参数(通过 EFI Loaded Image Protocol 获取)
// 这部分解释了为什么可以直接用 UEFI 启动内核:
// linux /vmlinuz root=UUID=xxx ro quiet
// 3. 内存分配与映射:调用 EFI GetMemoryMap() 获取物理内存布局
// 然后 ExitBootServices(),此后再也不能调用 EFI 引导服务
// 4. 跳转到真正的内核入口:kernel_entry()
// 这是 16位实模式 → 32位保护模式 → 64位长模式的切换过程
}
EFI Stub 的巧妙之处在于:内核既是操作系统的核心,也是引导阶段的参与者。它在内部完成了传统引导器(GRUB)所做的大部分工作——读取文件系统、加载 initrd、传递参数。
UEFI 启动 vs GRUB 启动的关键差异
| 特性 | GRUB 启动 | EFI Stub 启动 |
|---|---|---|
| 中间层 | 需要独立的 MBR/GPT 引导器和 GRUB | 无需中间引导器 |
| 文件系统支持 | GRUB 需内嵌 ext4/xfs 等驱动 | 内核自带 EFI FAT 驱动 |
| Secure Boot | GRUB 需签名,链式验证 | 内核直接签名验证 |
| 灵活性 | 菜单选择多内核 | 每个内核独立 EFI 条目 |
| 启动速度 | 多阶段跳转 | 单步直达 |
ARM64 平台则更简洁:booti 命令或 PSCI 直接跳转到内核入口(stext),没有实模式→保护模式的转换。
三、内核的解压缩与实模式入口
当 EFI Stub 准备好所有参数后,控制权通过 kernel_entry() 转到 arch/x86/boot/header.S 中的 _start 符号。这段 16 位实模式代码负责:
# arch/x86/boot/header.S 简化流程
_start:
# 1. 设置段寄存器,建立临时栈
cli
movw %cs, %ax
movw %ax, %ds
movw %ax, %ss
movw $0x7c00, %sp
# 2. 调用 BIOS 中断获取内存布局(E820 映射)
int $0x15
# 3. 切换到 32 位保护模式(设置 CR0.PE=1)
movl %cr0, %eax
orl $0x1, %eax
movl %eax, %cr0
ljmp $CODE32_SEG, $protcseg
# 4. 再切换到 64 位长模式(设置 EFER.LME=1,CR0.PG=1)
movl $MSR_EFER, %ecx
rdmsr
orl $EFER_LME, %eax
wrmsr
# 5. 跳转到 64 位代码:startup_64
jmp startup_64
startup_64 执行内核解压缩。为什么内核需要自解压?因为存储在磁盘上的 vmlinuz 是一个 bzImage(大 zImage),它由以下部分组成:
┌─────────────────────────────────────────────┐
│ setup.bin (16位+32位代码,512字节头部起) │
│ ├── 实模式入口 │
│ ├── 保护模式切换 │
│ └── 64位解压缩代码 (extract_kernel) │
├─────────────────────────────────────────────┤
│ vmlinux.bin.gz (压缩的内核主体) │
│ ├── .text (所有核心代码段) │
│ ├── .data (已初始化数据) │
│ └── .bss (未初始化数据) │
├─────────────────────────────────────────────┤
│ initramfs (可选,嵌入的初始 RAM 文件系统) │
└─────────────────────────────────────────────┘
解压缩完成后,vmlinux(未压缩的 ELF 格式内核)被加载到 CONFIG_PHYSICAL_START 指定的物理地址,最终跳转到 x86_64_start_kernel() → start_kernel()。
四、start_kernel():内核的"main"函数
init/main.c 中的 start_kernel() 是 C 语言进入内核后的第一个函数,也是整个内核初始化最集中、最重要的函数。它的执行流程大致如下:
// 高度简化的 start_kernel 调用链
asmlinkage __visible void __init start_kernel(void)
{
// ===== 早期初始化(中断关闭状态)=====
setup_arch(&command_line); // 架构特定初始化:内存映射、 EFI 参数解析
mm_init_cpumask(&init_mm); // 初始化内存描述符
init_IRQ(); // 注册中断控制器(APIC/IO-APIC/GIC)
time_init(); // 初始化时钟源(TSC/HPET/ACPI PM)
console_init(); // 初始化控制台(printk 可用!)
// ===== 内存管理 =====
bootmem_init(); // 引导内存分配器初始化
setup_per_cpu_pageset(); // 每 CPU 页面管理
build_all_zonelists(); // 构建内存区域列表
// ===== 调度器和 SMP =====
sched_init(); // 调度器初始化(CFS/EEVDF)
smp_prepare_boot_cpu(); // 准备 BSP(Bootstrap Processor)
smp_init(); // 初始化 AP(Application Processor),唤醒所有 CPU
init_timers(); // 初始化定时器框架
// ===== VFS 和根文件系统 =====
vfs_caches_init_early(); // VFS 缓存早期初始化
vfs_caches_init(); // 完整 VFS 初始化(dentry/inode 缓存)
rootfs_init(); // 注册 rootfs 伪文件系统
// ===== 关键:挂载 initramfs 作为初始根文件系统 =====
setup_binfmt_elf(); // 注册 ELF 格式加载器
}
在 start_kernel() 末尾(或 rest_init() 中),内核会尝试执行用户空间的第一个进程。传统的寻找顺序是:
/sbin/init → /etc/init → /bin/init → /bin/sh
如果都找不到,内核会 panic:
"Warning: unable to open an initial console"
"Failed to execute /sbin/init"
Kernel panic - not syncing: No working init found
五、initramfs:临时根文件系统的秘密
5.1 为什么需要 initramfs
在一个典型的生产环境中,根文件系统可能存在多种存储后端:
- 加密分区:LUKS 需要密码/TPM 解锁后才能挂载
- LVM/RAID: 需要先运行
mdadm/vgscan激活卷组 - 网络根文件系统:NFS/iSCSI 需要配置网络接口和发起方
- 多路径存储:需要
multipathd聚合路径
initramfs(initial RAM filesystem)是一个临时的、基于 RAM 的根文件系统,它在真正的根文件系统挂载之前提供必要的驱动和工具。
5.2 initramfs 的结构
initramfs 使用 cpio 归档 + gzip 压缩 格式。查看其结构:
# 提取 initramfs 内容
zcat /boot/initramfs-$(uname -r).img | cpio -idmv
典型的布局:
/
├── bin # 静态链接的 busybox 或必要工具
├── sbin # cryptsetup, lvm, mdadm 等
├── lib # 共享库和内核模块
│ └── modules/6.x.x/
│ └── kernel/
│ ├── drivers/md/ # RAID 驱动
│ ├── drivers/ata/ # SATA/NVMe 驱动
│ ├── fs/ext4/ # 文件系统驱动
│ └── crypto/ # 加密算法模块
├── etc # 配置文件
├── usr # 用户空间工具
├── init # 执行的第一个程序(/init)
└── run # 运行时目录
5.3 init(/init)的执行流程
当内核挂载 initramfs 后,执行其中的 /init 脚本。这个脚本的传统实现是从 SysV init 时代继承的 nash(Red Hat)或 busybox init。在现代 Linux 发行版中,几乎所有主流发行版都已切换到 systemd 作为 initramfs 的 init:
#!/bin/bash
# systemd-based initramfs /init 简化流程
# 1. 挂载必要的虚拟文件系统
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
mount -t efivarfs efivarfs /sys/firmware/efi/efivars # UEFI 变量访问
# 2. 解码内核命令行参数
# rd.luks.uuid=, root=, rd.md.uuid= 等
# 3. 加载必要的内核模块(udev 触发)
udevd --daemon
admamevent ;# 触发 udev 规则
# 4. 等待设备就绪(关键步骤!)
# rd.retry=30 rd.timeout=30 控制等待超时
udevadm settle
# 5. 解锁加密卷(交互式或 TPM 自动解锁)
cryptsetup luksOpen /dev/nvme0n1p2 cryptroot \
--key-file /run/keys/tpm-key \
--tpm2-device=auto
# 6. 激活 LVM/RAID
pvscan --cache
vgchange -ay
mdadm --assemble --scan
# 7. 挂载真正的根文件系统
mount /dev/mapper/vg-root /sysroot
# 8. switch_root:切换到真正的根文件系统
# 这是关键的系统调用!
exec switch_root /sysroot /sbin/init
switch_root 系统调用的核心作用是:将 initramfs 的内存释放,将真正的根文件系统挂载到 /,然后执行新系统的 init。这是一个不可逆操作——一旦执行,当前 PID 1 的用户空间代码被替换。
5.4 常见的 initramfs 故障排查
# 进入 initramfs shell 进行调试
# 在 GRUB 命令行中添加:rd.break=pre-mount 或 rd.shell
# 常用诊断命令
dmesg | grep -i "fail\|error\|unable"
cat /run/initramfs/rdsosreport.txt # dracut 提供的诊断报告
ls /dev/mapper/ # 检查加密映射
blkid # 查看 UUID 是否匹配
# 手动解锁 LUKS
cryptsetup open /dev/sda2 cryptroot
# 检查文件系统
fsck /dev/mapper/cryptroot
六、UKI:统一内核镜像的革命
6.1 UKI 的诞生背景
传统 Linux 内核启动的痛点:
- 碎片化管理:内核、initrd、命令行、微码更新分散在不同文件
- 可重现性差:手动编辑 GRUB 配置导致环境漂移
- 不可变基础设施不友好:OTA 更新需要修改引导配置
- 签名困难:grub.cfg 不受 Secure Boot 保护
Unified Kernel Image(UKI) 应运而生。UKI 是一个独立的 PE/COFF 内核映像,内部封装了:
┌─────────────────────────────────────────────────────┐
├── UEFI 应用程序头部 │
│ └── .linux → vmlinuz (压缩内核) │
│ └── .initrd → initramfs (cpio.gz) │
│ └── .cmdline → UTF-16 编码的内核命令行 │
│ └── .ucode → CPU 微码更新(intel-ucode.img) │
│ └── .splash → 启动画面 BMP │
│ └── .sbst → Secure Boot 签名表(可选) │
│ └── .pcrsig → TPM PCR 签名策略(systemd-stub) │
│ └── .pcrpkey → TPM PCR 公钥 │
└─────────────────────────────────────────────────────┘
6.2 systemd-stub:UKI 的核心魔法
从 systemd 250 开始引入的 systemd-stub 是处理 UKI 的核心 UEFI 驱动程序。它的职责包括:
- 解析 PE 内部 Section,加载
.linux和.initrd - 合并内核命令行:UKI 内置的
.cmdline+ UEFI 启动时传递的动态参数 - Measured Boot:测量 UKI 内容到 TPM PCR 7
- TPM PCR 策略:解锁 LUKS 加密卷时可绑定 PCR 值,防止 UKI 被篡改后解锁
- SSH in initramfs:允许远程解锁加密卷
6.3 构建 UKI 的工具链
主流发行版的 UKI 构建工具:
# 方法1:使用 ukify(systemd 252+ 自带)
ukify build \
--linux=/boot/vmlinuz-6.10.0 \
--initrd=/boot/initrd.img-6.10.0 \
--cmdline="root=UUID=abc-123 rw quiet" \
--ucode=/boot/intel-ucode.img \
--splash=/boot/splash.bmp \
--output=/efi/EFI/Linux/linux-6.10.0-abc123.efi \
--sign-kernel
# 方法2:Fedora/RHEL 通过 kernel-install
kernel-install add $(uname -r) /lib/modules/$(uname -r)/vmlinuz
# 方法3:NixOS 原生支持 UKI(高度可重现的构建)
# /etc/nixos/configuration.nix
boot.loader.efi.efiSysMountPoint = "/efi";
boot.loader.systemd-boot.enable = true;
boot.kernelPackages = pkgs.linuxPackages_latest;
6.4 UKI 在不可变基础设施中的应用
UKI 方案在以下场景中展现出巨大优势:
Fedora CoreOS / Fedora Atomic:
/efi/EFI/Linux/
├── fcos-x86.efi # 当前版本
├── fcos-x86.efi.rollback # 上一版本(自动回滚用)
└── loader/
└── entries/
└── fcos.conf # 极简配置(指向 UKI)
Endless OS 的 ostree + UKI:结合 OSTree 的版本化文件系统,实现原子更新和回滚。Azure Linux(CBL-Mariner)也采用 UKI 方案。
TPM 自动解锁:UKI 内置的 PCR 策略可以将 LUFS 密钥与特定的 UKI 哈希绑定:
# 将 LUKS 密钥与 TPM PCR 7(Secure Boot 状态)绑定
systemd-cryptenroll --tpm2-device=auto \
--tpm2-pcrs=0+7 \
/dev/nvme0n1p2
# 解密流程:
# UKI 加载时 → systemd-stub 测量内容到 PCR 7
# → TPM 解封密封的 LUKS 密钥
# → cryptsetup 解锁 → 挂载根文件系统
七、现代启动调试技术
7.1 利用 systemd-analyze 分析启动时间
# 生成启动流程 SVG 图
systemd-analyze plot > boot-analysis.svg
# 查看关键链(串行化的关键路径)
systemd-analyze critical-chain
# graphical.target @12.345s
# └─multi-user.target @12.340s
# └─network-online.target @11.500s
# └─NetworkManager-wait-online.service @9.800s +1.700s ← 瓶颈
7.2 内核启动参数速查
# 调试参数
earlyprintk=serial,ttyS0,115200 # 早期串口输出(关键调试手段)
initcall_debug # 打印所有 initcall 执行时间和参数
ignore_loglevel # 打印所有级别的日志
# 存储相关
rootdelay=10 # 等待设备就绪的时间(慢速 USB 盘)
rootfstype=ext4 # 显式指定根文件系统类型
rootflags=discard # 启用 TRIM
# initramfs 调试
rd.break=pre-mount # 在挂载前进入 shell
rd.shell # 出错时进入 shell
rd.retry=30 # 设备重试次数
# UKI 相关
systemd.stub=debug # 输出 systemd-stub 调试日志
7.3 UEFI Shell 级别的调试
# 在 UEFI Shell 中直接加载内核(绕过引导器)
FS0:
cd \EFI\Linux
linux-6.10.efi root=UUID=xxx rw debug initcall_debug
# 查看 EFI 变量
dmpstore -all # 列出所有 EFI 变量
dmpstore BootOrder # 查看启动顺序
八、从 Rust 工具链操作 UKI
虽然 UKI 构建通常由 ukify 或 kernel-install 完成,但理解其 PE 格式的底层操作对于定制场景很有价值。以下示例展示如何用 Rust 构建一个简化的 UKI 分析工具:
use goblin::pe::PE;
use std::fs;
/// UKI Section 解析器
fn parse_uki_sections(uki_path: &str) -> Result<(), Box<dyn std::error::Error>> {
let buffer = fs::read(uki_path)?;
let pe = PE::parse(&buffer)?;
println!("UKI: {}", uki_path);
println!("Sections:");
for section in &pe.sections {
let name = String::from_utf8_lossy(§ion.name);
println!(
" {:<12} VA: 0x{:08X} Size: {} bytes Characteristics: 0x{:08X}",
name.trim_end_matches('\0'),
section.virtual_address,
section.virtual_size,
section.characteristics
);
// 解析 cmdline 段(UTF-16 编码)
if name.starts_with(".cmdline") {
let data = &buffer[section.pointer_to_raw_data as usize
..(section.pointer_to_raw_data + section.size_of_raw_data) as usize];
// 去除末尾的 NULL bytes
let trimmed = data.split(|&b| b == 0).next().unwrap_or(data);
let cmdline = String::from_utf16le(trimmed)
.unwrap_or_else(|_| String::from("(invalid UTF-16)"));
println!(" → Kernel cmdline: {}", cmdline);
}
}
Ok(())
}
/// 验证 UKI 的 Secure Boot 签名
fn verify_uki_signature(uki_path: &str) -> Result<bool, Box<dyn std::error::Error>> {
// 实际实现需要解析 PE 的 Certificate Table (IMAGE_DIRECTORY_ENTRY_SECURITY)
// 并验证 PKCS#7 签名
// 这里简化为检查是否存在签名段
let buffer = fs::read(uki_path)?;
let pe = PE::parse(&buffer)?;
let cert_dir = &pe.header.optional_header.unwrap().data_directories
.certificate_table;
if cert_dir.virtual_address > 0 {
println!("UKI 包含安全签名 (大小: {} bytes)", cert_dir.size);
Ok(true)
} else {
println!("UKI 未签名");
Ok(false)
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!("用法: {} <uki-path>", args[0]);
std::process::exit(1);
}
parse_uki_sections(&args[1]).unwrap();
verify_uki_signature(&args[1]).unwrap();
}
对应的 Cargo.toml:
[package]
name = "uki-tool"
version = "0.1.0"
edition = "2021"
[dependencies]
goblin = "0.8"
九、生产环境最佳实践
9.1 双分区 OTA 更新策略
UKI 方案天然适合 A/B 更新:
ESP 分区 (1GB, FAT32)
├── EFI/
│ ├── Boot/
│ │ └── bootx64.efi # systemd-boot(极简引导器)
│ └── Linux/
│ ├── vmlinuz-6.10-A.efi # UKI A (当前)
│ └── vmlinuz-6.10-B.efi # UKI B (备用/更新中)
系统分区 (ext4/xfs)
├── A/ # 当前运行的根文件系统
└── B/ # 更新期间的临时写入目标
更新流程:
1. 将新 UKI 写入 B 槽位
2. 写入新的 B 分区系统镜像
3. 设置 EFI 变量 BootNext 指向 B 槽 UKI
4. 重启 → 新系统启动
5. 健康检查通过 → 标记 B 为默认 → 下次启动从 B
6. 健康检查失败 → 回滚到 A
9.2 Secure Boot 签名链
平台密钥 (PK) ─┐
├→ 密钥交换密钥 (KEK)
│ ├→ 签名数据库 (db)
│ │ ├→ systemd-boot.efi 签名
│ │ └→ UKI (包含 vmlinuz+initrd+cmdline)
│ └→ 禁止签名数据库 (dbx)
└→ 安全启动启用标志
9.3 initramfs 大小优化
# 优化 initramfs 大小(对快速启动和内存受限环境很重要)
# dracut 配置
cat > /etc/dracut.conf.d/optimize.conf << 'EOF'
# 仅包含必要的驱动
drivers+="ata nvme ext4"
omit_drivers+="snd drm gpu net"
# 使用 zstd 压缩(比 gzip 快 3-5 倍,压缩率略差)
compress="zstd"
# 不包含不必要的工具
omit_items+="bash vim man-pages"
EOF
dracut --force /boot/initrd.img $(uname -r)
十、总结
Linux 内核启动流程是一个精密的多层系统:
- UEFI 固件提供标准化的硬件初始化和运行时接口
- EFI Stub使内核可直接启动,无需传统引导器
- self-extracting bzImage处理架构特定的模式切换(实模式→保护模式→长模式)
- start_kernel()执行全局初始化,构建内存、中断、调度、VFS 等基础设施
- initramfs提供临时用户空间,完成根文件系统就绪前的准备工作
- UKI将内核、initramfs、cmdline、ucode 统一为单一 PE 文件,是 Serverless 容器和不可变基础设施的理想选择
掌握这些机制不仅能让你在启动故障时快速定位问题,更能帮助你在云原生时代设计更安全、更可维护的系统。随着 UKI 方案的成熟和普及,传统的 GRUB + vmlinuz + initrd 三元组正在被"单一可签名 UKI 文件"所替代——这不仅是格式的变革,更是运维理念的进化。

发表评论 取消回复