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 驱动程序。它的职责包括:

  1. 解析 PE 内部 Section,加载 .linux 和 .initrd
  2. 合并内核命令行:UKI 内置的 .cmdline + UEFI 启动时传递的动态参数
  3. Measured Boot:测量 UKI 内容到 TPM PCR 7
  4. TPM PCR 策略:解锁 LUKS 加密卷时可绑定 PCR 值,防止 UKI 被篡改后解锁
  5. 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(&section.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 文件"所替代——这不仅是格式的变革,更是运维理念的进化。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部