4. 启动阶段二:内核解压程序(decompressor)的入口与初始化
当引导加载程序(如 GRUB)将压缩后的内核映像(vmlinuz/bzImage)加载到内存并跳转到 启动扇区(boot sector) 后,控制权正式移交给内核自身的解压程序。这一阶段的核心目标是将压缩的内核映像解压到物理内存的指定位置,并准备好进入保护模式或长模式,为后续的 KASLR 地址随机化奠定基础。
4.1 解压程序的入口点:startup_32 / startup_64
根据内核架构(32 位或 64 位),解压程序的入口点有所不同。在 x86_64 架构下,入口点为 startup_64,它位于 arch/x86/boot/compressed/head_64.S 文件中。该入口点负责执行以下关键任务:
- 设置初始栈指针:为解压程序提供临时的栈空间。
- 保存引导信息:将引导加载程序传递的
boot_params结构体地址保存到寄存器中。 - 检查 CPU 模式:确认当前 CPU 是否处于长模式(64 位模式),如果不是,则进行模式切换。
- 建立页表:为解压过程建立临时的恒等映射页表(identity mapping),确保解压后的内核能够正确访问物理内存。
startup_64 运行在物理地址空间,此时虚拟地址尚未启用。所有内存访问都基于物理地址。
4.2 解压前的初始化流程
在调用解压函数之前,内核需要完成一系列初始化步骤,以确保解压环境的安全与稳定。下表总结了这些步骤及其作用:
| 步骤 | 函数/汇编段 | 主要作用 |
|---|---|---|
| 1. 设置栈指针 | movq $boot_stack_end, %rsp |
为解压程序提供可用的栈空间,防止函数调用时栈溢出。 |
| 2. 保存 boot_params | movq %rsi, %r15 |
将引导加载程序传递的参数结构体地址保存到 %r15 寄存器中,供后续使用。 |
| 3. 验证 CPU 特性 | verify_cpu |
检查 CPU 是否支持长模式、SSE2 等必要特性,若不支持则进入错误处理。 |
| 4. 建立恒等映射页表 | __startup_64 |
为解压程序自身和内核解压目标区域建立临时的 1:1 映射页表,确保分页开启后代码能正确执行。 |
| 5. 启用分页与长模式 | enable_paging |
设置控制寄存器 CR0、CR3、CR4 和 EFER,正式启用分页和 64 位长模式。 |
| 6. 调用解压函数 | call extract_kernel |
跳转到 C 语言编写的解压函数,执行实际的内核解压操作。 |
4.3 恒等映射页表与解压地址计算
在启用分页之前,解压程序必须建立恒等映射页表。这是因为解压程序本身的代码和即将解压的内核都位于物理内存中,而分页开启后,CPU 将使用虚拟地址访问内存。如果没有恒等映射,CPU 将无法找到下一条指令。
解压程序通过 __startup_64 函数计算当前物理地址与编译时链接地址之间的偏移量,并据此调整页表项。这一过程直接关系到 KASLR 的后续实现:
- 编译时基址:内核在编译时默认链接到一个固定的虚拟地址(例如
0xffffffff81000000)。 - 运行时基址:解压程序根据引导加载程序加载的实际物理地址,计算出运行时基址。
- 偏移量:运行时基址与编译时基址的差值,用于调整页表项和符号地址。
boot_params 指定的物理地址(通常为 0x1000000 或更高)。真正的随机化发生在解压完成后,内核主程序启动时。
4.4 解压程序初始化流程图
下图展示了从入口点到调用 extract_kernel 的完整初始化流程:
4.5 extract_kernel 函数解析
extract_kernel 是解压阶段的核心 C 函数,定义在 arch/x86/boot/compressed/misc.c 中。它接收以下参数:
- input_data:指向压缩内核数据的指针。
- input_len:压缩数据的长度。
- output:解压目标地址(物理地址)。
- output_len:解压后内核的大小。
- virt_addr:解压后内核期望运行的虚拟地址。
该函数的主要工作流程如下:
- 初始化解压算法:根据内核编译时选择的压缩算法(如 LZ4、LZMA、XZ、ZSTD),初始化对应的解压上下文。
- 执行解压:调用
__decompress函数,将压缩数据从input_data解压到output地址。 - 处理重定位:如果解压后的内核需要重定位(例如启用了 KASLR),则调用
relocate_kernel进行地址调整。 - 返回入口点:返回解压后内核的入口地址(通常为
startup_64在解压后内核中的位置)。
output 地址可能与解压程序自身的内存区域重叠。内核通过精心设计的解压顺序和临时缓冲区来避免覆盖正在执行的代码。
4.6 解压阶段的内存布局
理解解压阶段的内存布局对于分析 KASLR 至关重要。下表展示了典型的内存区域分配:
| 地址范围(物理) | 内容 | 说明 |
|---|---|---|
0x00000000 - 0x000FFFFF |
实模式中断向量表 / BIOS 数据区 | 保留区域,解压程序不使用。 |
0x00100000 - 0x001FFFFF |
引导加载程序暂存区 | GRUB 等可能在此存放模块或文件系统映像。 |
0x01000000 - 0x01FFFFFF |
解压程序自身(compressed kernel) | 包含 startup_64、解压代码和压缩后的内核数据。 |
0x02000000 - 0x02FFFFFF |
解压目标区域(output buffer) | 解压后的内核将被写入此区域,通常为 16MB 对齐。 |
0x03000000 - 0x03FFFFFF |
临时页表与栈 | 解压程序建立的恒等映射页表存放在此。 |
4.7 解压完成后的控制权移交
当 extract_kernel 成功返回后,解压程序执行最后的清理工作:
- 恢复引导参数:从保存的寄存器中恢复
boot_params地址。 - 跳转到解压后的内核:通过
ret指令或直接跳转,将控制权交给解压后内核的startup_64入口。
此时,解压后的内核已经位于物理内存的指定位置,并且页表已经建立。内核主程序将接管控制权,并开始执行包括 KASLR 随机化在内的后续初始化步骤。