4. 内核解压与初始化:start_kernel()之前的汇编与C语言入口

当引导加载程序(如GRUB、U-Boot)将压缩的内核映像加载到内存并跳转到入口点后,真正的内核启动之旅才刚刚开始。在调用众所周知的start_kernel()之前,内核必须经历一段“从汇编到C”的过渡期。这段代码负责解压内核、建立基本的页表、初始化CPU模式以及设置C语言运行环境。本章将深入剖析这一关键阶段。

4.1 内核入口点:从汇编开始

对于x86-64架构,内核的入口点定义在arch/x86/kernel/head_64.S中。对于ARM64,则是arch/arm64/kernel/head.S。这些汇编文件是内核执行的第一个指令。

关键概念: 入口点并非start_kernel(),而是汇编符号startup_64(x86-64)或stext(ARM64)。此时,内核仍处于压缩状态(如果启用了压缩),且运行在实模式或简单的页表模式下。

4.1.1 解压前的准备

在解压内核之前,汇编代码必须完成以下任务:

  1. 设置栈指针(Stack Pointer):为后续的C函数调用提供栈空间。
  2. 清零BSS段:确保未初始化的全局变量为零。
  3. 建立早期页表:启用分页,使内核能够访问高地址空间(如__START_KERNEL_map)。
  4. 跳转到解压代码:调用decompress_kernel()。
避坑指南: BSS段清零是C语言运行环境的基础。如果忘记清零,全局变量的初始值将是随机的,导致不可预测的崩溃。这也是为什么汇编阶段必须手动处理的原因。

4.2 内核解压:从压缩到可执行

现代内核通常使用gzip、xz或lz4等算法压缩。解压代码位于arch/x86/boot/compressed/或arch/arm64/boot/compressed/目录下。解压过程的核心是decompress_kernel()函数。

压缩算法 内核配置选项 解压速度 压缩比
gzip CONFIG_KERNEL_GZIP 中等 中等
xz CONFIG_KERNEL_XZ 慢 高
lz4 CONFIG_KERNEL_LZ4 快 低
zstd CONFIG_KERNEL_ZSTD 快 高
性能权衡: 在嵌入式系统中,解压速度直接影响启动时间。如果使用xz算法,虽然镜像更小,但解压时间可能增加数秒。对于启动时间敏感的设备(如汽车ECU),建议使用lz4或zstd。

4.2.1 解压流程

解压过程并非简单的“解压后跳转”。它涉及地址重定位和内存布局调整。以下是典型流程:

  1. 识别压缩格式:检查内核映像头部的魔数(magic number)。
  2. 分配解压缓冲区:在内存中预留一块区域用于存放解压后的内核。
  3. 执行解压:调用相应的解压函数(如__gunzip())。
  4. 重定位:如果解压后的地址与链接地址不一致,需要调整符号表。
  5. 跳转到解压后的入口:最终跳转到startup_64(解压后的版本)或__primary_switch。
内核解压与入口跳转流程 Bootloader跳转 汇编入口 startup_64 建立早期页表 解压内核 重定位与BSS清零 跳转到start_kernel 说明 解压后的内核映像被放置在物理内存的某个位置,然后通过重定位修正地址,最后跳转到start_kernel()。

4.3 从汇编到C的过渡:start_kernel()的入口

解压完成后,内核进入第二阶段汇编代码(如arch/x86/kernel/head64.c中的x86_64_start_kernel()),然后最终调用start_kernel()。这个函数是内核初始化C语言部分的起点,定义在init/main.c中。

4.3.1 start_kernel()之前的最后几步

在start_kernel()被调用之前,汇编代码必须确保:

  • CPU处于正确的模式:x86-64需要处于长模式(Long Mode),ARM64需要处于EL1(异常级别1)。
  • 页表已完全初始化:能够映射整个物理内存和内核代码段。
  • 中断已禁用:防止在初始化过程中被中断打断。
  • 栈指针有效:指向一个足够大的栈区域。
代码示例: 以下是x86-64架构中跳转到start_kernel()的典型汇编片段(简化):
/* arch/x86/kernel/head_64.S */
movq    %rax, %rsi          /* 将boot_params传递给start_kernel */
movq    initial_stack(%rip), %rsp  /* 设置栈指针 */
call    start_kernel        /* 进入C语言世界 */

4.4 早期页表与内存布局

在解压和初始化过程中,页表扮演着至关重要的角色。内核需要从物理地址(低地址)过渡到虚拟地址(高地址)。

4.4.1 身份映射与高地址映射

早期页表通常包含两种映射:

映射类型 虚拟地址范围 物理地址范围 用途
身份映射 0x0 ~ 内核大小 0x0 ~ 内核大小 确保解压代码能够访问自身
高地址映射 __START_KERNEL_map ~ 结束 0x0 ~ 内核大小 使内核运行在虚拟地址空间
为什么需要身份映射? 在启用分页后,CPU的指令指针(RIP)会立即变成虚拟地址。如果此时没有身份映射,CPU将无法获取下一条指令,导致三重故障(Triple Fault)和系统重启。
早期页表:身份映射与高地址映射 物理内存 0x0 ~ 内核大小 内核代码+数据 身份映射 虚拟地址 = 物理地址 高地址映射 虚拟地址 = 物理地址 + 偏移 关键点 身份映射保证 解压期间CPU 能正常取指。 高地址映射 使内核运行在 虚拟地址空间。

4.5 总结:汇编与C的协作

从引导加载程序跳转到内核,再到start_kernel()的执行,整个过程是汇编与C语言紧密协作的典范。汇编负责硬件初始化、页表建立和运行环境准备,而C语言则负责后续的高级抽象和策略管理。理解这一过渡阶段,对于调试启动问题、优化启动时间以及深入理解内核架构至关重要。

核心要点回顾:
  • 内核入口是汇编代码,不是start_kernel()。
  • 解压过程涉及地址重定位和内存布局调整。
  • 早期页表必须包含身份映射,以防止分页启用后CPU取指失败。
  • BSS清零和栈设置是C语言运行环境的基础。