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 解压前的准备
在解压内核之前,汇编代码必须完成以下任务:
- 设置栈指针(Stack Pointer):为后续的C函数调用提供栈空间。
- 清零BSS段:确保未初始化的全局变量为零。
- 建立早期页表:启用分页,使内核能够访问高地址空间(如
__START_KERNEL_map)。 - 跳转到解压代码:调用
decompress_kernel()。
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 |
快 | 高 |
4.2.1 解压流程
解压过程并非简单的“解压后跳转”。它涉及地址重定位和内存布局调整。以下是典型流程:
- 识别压缩格式:检查内核映像头部的魔数(magic number)。
- 分配解压缓冲区:在内存中预留一块区域用于存放解压后的内核。
- 执行解压:调用相应的解压函数(如
__gunzip())。 - 重定位:如果解压后的地址与链接地址不一致,需要调整符号表。
- 跳转到解压后的入口:最终跳转到
startup_64(解压后的版本)或__primary_switch。
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)。
- 页表已完全初始化:能够映射整个物理内存和内核代码段。
- 中断已禁用:防止在初始化过程中被中断打断。
- 栈指针有效:指向一个足够大的栈区域。
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 ~ 内核大小 | 使内核运行在虚拟地址空间 |
4.5 总结:汇编与C的协作
从引导加载程序跳转到内核,再到start_kernel()的执行,整个过程是汇编与C语言紧密协作的典范。汇编负责硬件初始化、页表建立和运行环境准备,而C语言则负责后续的高级抽象和策略管理。理解这一过渡阶段,对于调试启动问题、优化启动时间以及深入理解内核架构至关重要。
- 内核入口是汇编代码,不是
start_kernel()。 - 解压过程涉及地址重定位和内存布局调整。
- 早期页表必须包含身份映射,以防止分页启用后CPU取指失败。
- BSS清零和栈设置是C语言运行环境的基础。