3. 内核启动入口:head.S与启动汇编、从实模式到保护模式、页表初始化
本章深入剖析Linux内核启动的早期阶段,从CPU加电复位后的第一条指令开始,直至开启分页机制、进入保护模式。这一过程是内核启动的基石,也是理解CPU热插拔与SMP协同的前提——每个CPU核心在启动时都会经历类似的初始化路径。
3.1 启动流程总览:从BIOS到内核入口
当x86系统上电或复位时,CPU处于实模式(Real Mode),CS:IP强制指向0xFFFF:0xFFF0(即物理地址0xFFFFFFF0),该地址映射到BIOS ROM。BIOS完成硬件自检(POST)后,加载引导扇区(MBR/GPT)到内存0x7C00处,并将控制权交给引导程序(如GRUB2)。
引导程序负责加载内核映像(bzImage)到内存,并跳转到内核的启动扇区(arch/x86/boot/header.S中的start_of_setup)。随后,内核经历以下关键阶段:
| 阶段 | 代码位置 | CPU模式 | 核心任务 |
|---|---|---|---|
| 1. 启动扇区 | arch/x86/boot/header.S | 实模式 | 设置堆栈、解析引导参数、切换到保护模式前的准备 |
| 2. 保护模式切换 | arch/x86/boot/pmjump.S | 实模式→保护模式 | 加载GDT、设置CR0.PE位、远跳转刷新流水线 |
| 3. 32位启动内核 | arch/x86/boot/compressed/head_32.S | 保护模式(32位) | 解压内核、设置页表、开启分页 |
| 4. 64位内核入口 | arch/x86/kernel/head_64.S | 长模式(64位) | 初始化页表、设置GS/FS段、跳转到start_kernel |
3.2 实模式下的启动汇编:header.S与setup代码
内核映像bzImage由两部分组成:setup代码(实模式部分)和压缩的内核(保护模式部分)。setup代码位于映像开头,由引导程序加载到物理地址0x10000(64KB)处,随后跳转到start_of_setup。
实模式汇编的关键操作包括:
- 设置堆栈:将SS:SP指向一个安全的临时堆栈区域,避免覆盖关键数据。
- 解析引导参数:从boot_params结构体中读取内存大小、视频模式、命令行参数等。
- 检测硬件:通过BIOS中断(如int 0x15)获取内存映射(E820)、APM/ACPI信息。
- 切换到保护模式:关闭中断(CLI)、加载全局描述符表(GDT)、设置CR0寄存器的PE位。
; arch/x86/boot/pmjump.S 关键片段
.code16
protected_mode_jump:
movl %edx, %esi ; 保存32位入口地址到esi
xorl %ebx, %ebx
movl %cr0, %eax
orb $1, %al ; 设置CR0.PE位,开启保护模式
movl %eax, %cr0
; 远跳转刷新指令流水线,加载CS段选择子
ljmpl $__BOOT_CS, $in_pm32
.code32
in_pm32:
movl %esi, %eax
jmp *%eax ; 跳转到32位内核入口
3.3 保护模式切换:GDT与段机制
保护模式的核心是段描述符表(GDT/LDT)。内核在实模式下预先构建一个临时的GDT,包含必要的代码段、数据段描述符。每个描述符指定段的基地址、限长、访问权限和类型。
临时GDT的结构如下:
| 索引 | 段选择子 | 用途 | 基地址 | 限长 | DPL |
|---|---|---|---|---|---|
| 0 | 0x00 | 空描述符(必须) | — | — | — |
| 1 | __BOOT_CS (0x10) | 32位代码段 | 0x00000000 | 0xFFFFFFFF | 0 (内核) |
| 2 | __BOOT_DS (0x18) | 32位数据段 | 0x00000000 | 0xFFFFFFFF | 0 (内核) |
切换完成后,CPU使用段选择子访问内存,逻辑地址通过段描述符转换为线性地址。此时尚未开启分页,线性地址直接对应物理地址。
3.4 页表初始化:从临时页表到内核页表
进入保护模式后,内核立即着手初始化页表,开启分页机制。对于32位内核,使用两级页表(页目录+页表);对于64位内核,使用四级或五级页表(PML4→PDPT→PD→PT)。
页表初始化的核心目标:
- 恒等映射:将内核所在的物理地址区域映射到相同的虚拟地址,确保开启分页后指令指针不失效。
- 高地址映射:将内核映射到虚拟地址空间的高端(如0xC0000000以上),为后续进程地址空间布局做准备。
- 覆盖关键区域:确保页表本身、GDT、IDT、堆栈等关键数据结构在分页后仍可访问。
; arch/x86/kernel/head_64.S 中页表初始化片段
__startup_64:
; 计算页表在内存中的位置(对齐到4KB)
leaq _text(%rip), %rdi
andq $~0xFFF, %rdi
; 清零页表区域
movl $512, %ecx ; 512个PML4E
xorl %eax, %eax
rep stosq
; 设置PML4E指向PDPT
leaq level2_kernel_pgt(%rip), %rax
orq $0x07, %rax ; Present + R/W + User
movq %rax, level1_kernel_pgt(%rip)
; ... 后续设置PDPT、PD、PT条目
3.5 启动阶段页表结构图
下图展示了64位内核在启动初期建立的临时页表结构,采用恒等映射与高地址映射并存的策略:
3.6 从实模式到长模式的完整路径
对于64位内核,CPU模式切换并非直接从实模式跳到长模式,而是经过一个中间步骤:先切换到保护模式(32位),再通过设置EFER寄存器的LME位和CR4的PAE位,最后开启CR0.PG位进入长模式。
- 实模式:16位,20位地址线,1MB地址空间,无保护。
- 保护模式(32位):32位,段保护,4GB地址空间,可开启分页。
- 长模式(64位):64位,必须启用PAE页表,支持64位虚拟地址,兼容32位子模式。
- CR0.PE:开启保护模式(位0)
- CR4.PAE:开启物理地址扩展(位5),长模式必须
- EFER.LME:开启长模式(位8)
- CR0.PG:开启分页(位31),必须在设置完页表后置位
3.7 启动汇编中的关键数据结构
在head.S和head_64.S中,内核定义了几个关键的数据结构,用于页表初始化和早期内存管理:
| 符号名称 | 类型 | 用途 | 对齐 |
|---|---|---|---|
| level1_kernel_pgt | PML4E数组 | 顶级页表(PML4),包含指向PDPT的条目 | 4KB |
| level2_kernel_pgt | PDPTE数组 | 页目录指针表,指向页目录 | 4KB |
| level3_kernel_pgt | PDE数组 | 页目录,包含2MB大页或指向页表的条目 | 4KB |
| level4_kernel_pgt | PTE数组 | 页表(仅当使用4KB页时存在) | 4KB |
| initial_code | 指针 | 存储start_kernel的入口地址 | 8字节 |
| initial_stack | 字节数组 | 早期内核堆栈(通常为16KB) | 16字节 |
; arch/x86/kernel/head_64.S 中数据段定义
.section .data
.align 4
.globl initial_code
initial_code:
.quad x86_64_start_kernel ; 指向C语言入口
.align 16
.globl initial_stack
initial_stack:
.fill 4096, 1, 0 ; 16KB堆栈空间
.quad 0 ; 堆栈顶标记
.align 4096
.globl level1_kernel_pgt
level1_kernel_pgt:
.quad level2_kernel_pgt + 0x07 ; Present + R/W + User
.fill 511, 8, 0 ; 其余条目清零
3.8 常见问题与调试技巧
- 页表未对齐:页表基地址必须4KB对齐,否则CPU在加载CR3时会触发通用保护异常(#GP)。
- GDT描述符错误:代码段描述符的S位(描述符类型)必须为1(代码/数据段),DPL必须与当前CPL匹配,否则远跳转会失败。
- 缓存一致性问题:修改页表后,必须使用INVLPG指令或重新加载CR3来刷新TLB,否则CPU可能使用过期的缓存映射。
- 栈指针无效:在切换模式时,如果SS:SP指向的地址在分页后不可访问,会导致双重异常(#DF)。
至此,内核完成了从实模式到保护模式、再到长模式的切换,并建立了基础的页表映射。CPU已经准备好跳转到C语言编写的start_kernel函数,开启内核初始化的下一阶段——在这里,CPU热插拔与SMP启动的协同机制将逐步展开。