3. 解压缩入口:arch/x86/boot/compressed/head_64.S 启动汇编入口分析

本章聚焦于 Linux 内核解压缩阶段的入口汇编文件 head_64.S,深入分析从实模式到保护模式,再到长模式(IA-32e)的完整切换流程。该文件是内核解压缩的起点,负责建立执行环境,为后续的 C 语言解压缩代码铺平道路。

3.1 入口点与启动约定

head_64.S 的入口点由引导加载程序(如 GRUB2)根据 64 位 ELF 头部直接跳转进入。此时 CPU 已处于 32 位保护模式,但尚未开启分页和长模式。该文件的核心任务包括:

  • 建立 64 位页表:为进入长模式准备虚拟地址映射。
  • 切换至长模式:设置 EFER.LME 并启用分页。
  • 重定位内核:将解压缩后的内核复制到最终运行地址。
  • 校验完整性:对压缩内核进行 CRC 校验。
关键点: 入口点 startup_64 是 64 位代码入口,但 CPU 在跳转前仍处于保护模式。这种“提前声明”的 64 位入口是 x86 启动协议的一部分,要求引导加载程序在跳转前已设置好基本的 32 位段描述符。

3.2 从保护模式到长模式的切换流程

切换过程分为三个主要阶段,每个阶段都有明确的寄存器状态和内存布局要求。下表总结了各阶段的关键特征:

阶段 CPU 模式 分页状态 GDT 要求 关键寄存器
入口 32 位保护模式 禁用 32 位平坦模型 CS.D=1, CR0.PE=1
过渡 保护模式(准备中) 启用(临时页表) 添加 64 位代码段 EFER.LME=1, CR3=页表基址
完成 IA-32e 长模式 启用(最终页表) 64 位代码段生效 CS.L=1, CR0.PG=1

3.3 核心汇编指令序列分析

以下代码片段展示了 head_64.S 中从保护模式切换到长模式的核心指令序列。该序列必须严格按顺序执行,任何一步出错都会导致系统崩溃。

/* head_64.S - 切换到长模式的关键步骤 */
.code32
startup_32:
    /* 1. 加载临时 GDT,包含 32 位和 64 位代码段描述符 */
    lgdt    gdt_ptr

    /* 2. 设置 CR4.PAE 启用物理地址扩展 */
    movl    %cr4, %eax
    orl     $X86_CR4_PAE, %eax
    movl    %eax, %cr4

    /* 3. 设置 EFER.LME 启用长模式 */
    movl    $MSR_EFER, %ecx
    rdmsr
    orl     $EFER_LME, %eax
    wrmsr

    /* 4. 设置 CR3 指向 4 级页表基址 */
    movl    $(boot_page_table - __START_KERNEL_map), %eax
    movl    %eax, %cr3

    /* 5. 启用分页(CR0.PG),此时 CPU 进入长模式 */
    movl    %cr0, %eax
    orl     $X86_CR0_PG, %eax
    movl    %eax, %cr0

    /* 6. 远跳转刷新指令流水线,加载 64 位代码段 */
    ljmp    $__KERNEL_CS, $startup_64
注意事项: 第 5 步启用分页后,CPU 立即进入长模式。但此时 CS 仍然是 32 位保护模式下的选择子,必须通过第 6 步的远跳转(ljmp)来加载 64 位代码段描述符,并刷新指令预取队列。如果省略此跳转,后续指令会被错误解码。

3.4 页表建立与地址映射

在进入长模式前,必须建立至少一个 4 级页表(PML4、PDPT、PD、PT)。head_64.S 使用静态定义的页表,将虚拟地址空间的前 2MB 映射到物理地址 0x0,确保解压缩代码能够正确访问。

/* 页表项定义(简化) */
.section ".pgtable","a",@nobits
.balign 4096
boot_page_table:
    /* PML4E: 指向第一个 PDPT */
    .quad   0x0000000000001007    /* 存在 + 读写 + 用户 */
    .fill   511, 8, 0

    /* PDPTE: 指向第一个 PD */
    .quad   0x0000000000002007    /* 存在 + 读写 + 用户 */
    .fill   511, 8, 0

    /* PDE: 2MB 大页映射 */
    .quad   0x0000000000000087    /* 存在 + 读写 + 大页 + 全局 */
    .fill   511, 8, 0
映射说明: 上述页表使用 2MB 大页(PS=1),减少了页表层级占用。第一个 PDE 将虚拟地址 0x0 映射到物理地址 0x0,并设置全局位(G=1),避免 TLB 刷新。这种映射在解压缩阶段保持不变,直到内核切换到最终页表。

3.5 模式切换状态机

下图展示了从实模式到长模式的完整状态转换过程,重点突出 head_64.S 负责的“保护模式 → 长模式”阶段。

模式切换状态机 实模式 保护模式 长模式 解压缩 重定位 校验 head_64.S 负责阶段 保护模式 → 长模式切换,包括页表建立、EFER 设置和远跳转

3.6 地址重定位与校验准备

进入长模式后,head_64.S 立即执行地址重定位。由于内核被加载的物理地址可能与链接地址不同,必须调整全局偏移表(GOT)和代码中的绝对地址引用。

/* 重定位核心逻辑 */
.code64
startup_64:
    /* 计算当前运行地址与链接地址的偏移 */
    leaq    _text(%rip), %rbp
    subq    $_text, %rbp

    /* 遍历并修正 GOT 表项 */
    leaq    got_table(%rip), %rdi
    leaq    got_table_end(%rip), %rsi
1:
    cmpq    %rsi, %rdi
    jae     2f
    addq    %rbp, (%rdi)
    addq    $8, %rdi
    jmp     1b
2:
    /* 继续执行解压缩前的校验 */
    call    verify_crc32
性能提示: 使用 leaq 指令获取 RIP 相对地址是 64 位模式下推荐的做法,避免了手动维护段寄存器的麻烦。重定位循环使用 addq $8, %rdi 遍历 8 字节指针,确保每个 GOT 表项都被修正。

3.7 关键数据结构对比

下表对比了保护模式和长模式下关键数据结构的差异,帮助理解切换过程中需要调整的内容:

数据结构 保护模式(32 位) 长模式(64 位) 切换影响
GDT 描述符 8 字节,基址 32 位 8 字节,基址 64 位 需添加 64 位代码段(CS.L=1)
页表项 4 字节(PAE 下 8 字节) 8 字节,物理地址 52 位 必须使用 PAE 或长模式页表
中断描述符表 8 字节,无 IST 16 字节,支持 IST 需重新设置 IDT
调用门 支持 32 位调用门 支持 64 位调用门 需使用 64 位门描述符

3.8 常见问题与调试建议

  • 页表未对齐:CR3 要求页表基址 4KB 对齐,否则 #GP 异常。
  • EFER.LME 设置顺序:必须在启用分页前设置,且不能与 CR0.PG 同时写入。
  • 远跳转目标:ljmp 的目标地址必须是 64 位代码段内的有效 RIP。
  • GOT 未修正:如果重定位循环遗漏某些表项,解压缩代码会访问错误地址。
调试技巧: 在 QEMU 中使用 -d int 可以捕获异常信息。如果切换后立即触发 #GP,检查 GDT 中 64 位代码段的 D/B 位是否清零(应为 0),以及 L 位是否置 1。使用 info registers 查看 EFER 和 CR0 的值,确认分页启用状态。