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 负责的“保护模式 → 长模式”阶段。
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 的值,确认分页启用状态。