4. 内核解压与入口:内核镜像解压过程、start_kernel函数入口、早期架构初始化
当引导加载程序(如 GRUB、UEFI 启动管理器)将控制权交给内核后,内核首先经历一段高度依赖架构的早期启动流程。这一阶段的核心任务包括:从压缩的内核镜像中解压出原始内核、设置基本的执行环境、然后跳转到与体系结构无关的通用 C 语言入口点 start_kernel。理解这一过程对于排查启动早期崩溃、解压失败或硬件初始化错误至关重要。
4.1 内核镜像的压缩与解压机制
为了节省存储空间并加快加载速度,Linux 内核镜像通常以压缩形式存储。常见的压缩算法包括 gzip、bzip2、LZMA、XZ、LZO 和 ZSTD。内核构建系统会在编译完成后,将原始 ELF 格式的内核(vmlinux)通过 objcopy 转换为纯二进制镜像,然后使用选定的压缩算法进行压缩,并在其头部附加一个自解压存根(decompressor stub)。
关键概念: 压缩后的内核镜像通常命名为
vmlinuz(zImage/bzImage 格式),其中包含一个小的、未压缩的头部代码,负责解压内核主体并跳转到解压后的入口点。
4.1.1 解压流程步骤
- 引导加载程序加载:引导加载程序将
vmlinuz加载到物理内存的特定位置(通常为 1MB 偏移处,即0x100000)。 - 实模式入口:CPU 首先在 16 位实模式下执行
arch/x86/boot/header.S中的代码,进行基本的硬件探测和设置。 - 切换到保护模式:实模式代码完成使命后,切换到 32 位保护模式,并跳转到
arch/x86/boot/compressed/head_64.S(或head_32.S)中的解压入口。 - 解压内核:解压存根代码识别压缩算法,将压缩的内核数据解压到指定的内存地址(通常为
0x1000000或更高)。 - 跳转到解压后的内核:解压完成后,存根代码跳转到解压后的内核入口点
startup_64(x86_64)或startup_32,最终进入start_kernel。
故障排查提示: 如果内核在解压阶段崩溃,通常会输出类似
"Decompressing Linux... done. Booting the kernel." 后无响应的日志。此时应检查内存布局冲突、压缩算法支持是否编译进内核,或引导加载程序传递的 initrd 地址是否与解压地址重叠。
4.1.2 解压过程架构图
4.2 start_kernel 函数入口
start_kernel 是内核启动过程中第一个与体系结构无关的 C 语言函数,定义在 init/main.c 中。它是内核初始化的总控中心,负责调用所有核心子系统的初始化函数。在进入 start_kernel 之前,架构相关的汇编代码已经完成了页表、中断描述符表(IDT)、全局描述符表(GDT)等基本设置。
4.2.1 start_kernel 的早期调用链
| 调用顺序 | 函数名称 | 功能描述 |
|---|---|---|
| 1 | setup_arch() |
架构特定初始化,解析启动参数、设置内存布局、初始化页表 |
| 2 | trap_init() |
初始化中断向量表,设置异常处理程序 |
| 3 | mm_init() |
内存管理子系统初始化,包括伙伴系统和 slab 分配器 |
| 4 | sched_init() |
进程调度器初始化,设置 idle 进程和调度类 |
| 5 | init_IRQ() |
中断控制器初始化,注册 IRQ 处理函数 |
| 6 | console_init() |
初始化控制台,使内核日志能够输出到屏幕或串口 |
| 7 | vfs_caches_init() |
虚拟文件系统缓存初始化,为挂载根文件系统做准备 |
| 8 | rest_init() |
创建内核线程 kernel_init 和 kthreadd,进入 idle 循环 |
常见故障点: 如果
setup_arch() 阶段解析启动参数失败(如无效的 mem= 参数),内核可能在早期就 panic。另外,console_init() 之前的日志无法通过标准控制台输出,只能通过早期打印(early_printk)或硬件调试器捕获。
4.2.2 start_kernel 早期初始化流程图
4.3 早期架构初始化
早期架构初始化是 start_kernel 中第一个被调用的关键步骤,由 setup_arch() 函数完成。该函数高度依赖具体硬件平台,以 x86_64 为例,它执行以下核心任务:
- 解析内核命令行:处理引导加载程序传递的启动参数,如
mem=、acpi=、pci=等。 - 内存布局探测:通过
e820表或 UEFI 内存映射,确定物理内存的可用区域和保留区域。 - 页表初始化:建立早期页表,启用分页机制,为内核虚拟地址空间映射提供基础。
- CPU 特性检测:通过
CPUID指令检测 CPU 支持的特性(如 SSE、AVX、SMEP、SMAP 等)。 - ACPI 表解析:定位并解析 ACPI 根表,为电源管理和设备枚举做准备。
4.3.1 架构初始化关键数据结构
| 数据结构 | 所在文件 | 作用 |
|---|---|---|
boot_params |
arch/x86/include/asm/bootparam.h |
存储引导加载程序传递的硬件信息,包括 e820 表、命令行、视频模式等 |
e820_table |
arch/x86/kernel/e820.c |
物理内存布局表,标记可用、保留、ACPI 等内存区域 |
pgd_t / pud_t / pmd_t / pte_t |
arch/x86/include/asm/pgtable_types.h |
四级页表条目类型,用于虚拟地址到物理地址的转换 |
cpuinfo_x86 |
arch/x86/include/asm/processor.h |
存储 CPU 型号、步进、特性标志等详细信息 |
4.3.2 早期架构初始化流程(x86_64)
4.4 常见启动故障与排查方法
核心原则: 内核启动早期的故障通常表现为无输出、死机或重启。由于控制台尚未初始化,标准
printk 日志不可用。此时应依赖 early_printk、串口调试或 QEMU 等虚拟化工具进行排查。
4.4.1 故障现象与原因对照表
| 故障现象 | 可能原因 | 排查方法 |
|---|---|---|
| 解压完成后无任何输出 | 解压地址与 initrd 或保留内存重叠;页表设置错误 | 使用 memmap= 参数排除冲突区域;启用 early_printk |
内核 panic 在 setup_arch 中 |
无效的启动参数;e820 表损坏;ACPI 表解析失败 | 检查引导加载程序传递的参数;使用 acpi=off 测试 |
| CPU 特性检测导致死机 | CPU 微码不兼容;内核编译时启用了不支持的指令集 | 更新 BIOS/微码;使用 clearcpuid= 禁用特定特性 |
| 控制台无输出但系统仍在运行 | console_init 失败;串口参数配置错误 |
添加 console=ttyS0,115200 等参数;检查硬件连接 |
4.4.2 启用早期调试输出的方法
// 在 kernel 命令行中添加以下参数启用早期打印
earlyprintk=serial,ttyS0,115200
// 或使用 VGA 控制台
earlyprintk=vga
// 对于 x86 架构,可以在 arch/x86/kernel/head64.c 中手动添加
// early_printk 调用,但需要确保 early_printk 已编译进内核
// 配置选项:CONFIG_EARLY_PRINTK=y
实战建议: 在开发或调试自定义内核时,始终在
start_kernel 的开头添加一个 early_printk 输出,以确认内核是否成功进入 C 代码入口。这是区分“解压失败”和“初始化失败”的最直接方法。