4、start_kernel之前:分析decompress_kernel()后的控制权交接,进入C世界的入口点

好,我们接着往下走。上一节我们把内核镜像解压完了,现在内核已经躺在内存里了。但问题来了——解压完了,然后呢?谁把控制权交给真正的内核?怎么交的?

说白了,decompress_kernel() 干完活之后,系统还处于一个“半生不熟”的状态。这时候的 CPU 还在实模式或者某个临时页表下跑着,内存管理还没完全初始化,连 C 语言运行环境都还没搭好。嗯,这里就是整个启动流程中最微妙的一个环节——从汇编跳进 C 世界。

4.1 解压后的“烂摊子”

我个人习惯把 decompress_kernel() 执行完毕后的状态叫做“烂摊子”。为什么这么说?你想想看:

  • 解压后的内核镜像在内存里,但它的入口地址在哪?
  • CPU 还在用着解压阶段的临时栈和页表
  • 中断还是关着的
  • 连最基本的 BSS 段都没清零

我在项目中遇到过一回,自己改了个压缩算法,结果解压完跳转地址算错了,系统直接跑飞。查了两天才发现是那个偏移量没对齐。所以这个地方,真的得一个字一个字地抠。

4.2 关键的跳转:从汇编到 C

解压完成后,控制权交接的核心代码在 arch/x86/boot/compressed/head_64.S(以 x86_64 为例)。关键片段长这样:

/*
 * 解压完成后,跳转到 decompressed kernel
 */
leaq    boot_heap(%rip), %rsp   /* 切换栈 */
call    extract_kernel          /* 调用解压函数 */
...
/* 解压返回后,%rax 里存的是解压后内核的入口地址 */
pushq   %rax                    /* 把入口地址压栈 */
ret                             /* 跳过去! */

看到没?最后那个 ret 指令,就是进入 C 世界的“传送门”。%rax 里存的是 startup_64 的地址——也就是解压后内核真正的入口点。

核心要点:decompress_kernel() 返回后,%rax 寄存器里装的就是 start_kernel() 的入口地址。这个地址是解压阶段根据内核在内存中的实际位置计算出来的。

4.3 进入 C 世界前的最后准备

跳过去之前,还有几件事必须做完。我列一下:

  1. 切换栈:从解压用的临时栈,切到内核自己的栈
  2. 设置页表:至少保证能访问到内核所在的物理内存
  3. 清 BSS:把未初始化的全局变量区域清零
  4. 保存启动信息:比如 boot_params 结构体的地址

这些事都是在汇编里干的。为什么?因为 C 语言运行环境还没准备好——连栈指针都不对,你怎么调用 C 函数?

一个小技巧:我调试启动流程时,经常在 ret 指令前加个 hlt 断点。然后用 JTAG 读 %rax 的值,确认跳转地址对不对。这比看 log 靠谱多了。

4.4 进入 start_kernel():C 世界的起点

跳转成功后,CPU 开始执行 startup_64。这个函数在 arch/x86/kernel/head_64.S 里,但它很快会调用 x86_64_start_kernel(),然后一路走到 start_kernel()

嗯,这里有个细节很多人会忽略:start_kernel() 虽然是 C 函数,但它被调用时,CPU 还处于一个“半初始化”状态。比如:

  • 中断还是关的(local_irq_disable()
  • 只有一个 CPU 在跑(其他核还在睡觉)
  • 内存管理还没初始化(连 kmalloc 都不能用)

所以 start_kernel() 的前几百行代码,都是在做“初始化初始化环境”的工作。说白了,就是给 C 世界铺好地板、装好门窗。

4.5 一张图看懂控制权交接

我画了张流程图,把整个交接过程串起来。你看一眼就明白了:

decompress_kernel() 后的控制权交接流程 decompress_kernel() 完成 解压后的内核在内存中 返回 %rax 汇编收尾工作 切换栈 / 清 BSS / 设页表 ret 指令跳转 进入 startup_64 start_kernel() C 世界的真正起点 关键寄存器状态 • %rax = 内核入口地址 • %rsp = 内核栈指针 • %cr3 = 临时页表 • EFLAGS.IF = 0 (关中断) ⚠ 此时还不能用 kmalloc

4.6 避坑指南

我曾经在这个交接点上栽过跟头,分享几个经验:

坑1:栈指针没切干净

解压阶段的栈和内核栈可能在不同的内存区域。如果 %rsp 没切对,进了 start_kernel() 后一压栈就把解压阶段的数据给踩了。我遇到过一回,现象是内核启动到一半莫名其妙死掉,查了两天才发现是栈重叠了。

坑2:页表覆盖范围不够

解压阶段的页表可能只映射了解压缓冲区。如果内核镜像被解压到了页表覆盖范围之外,跳转过去直接 page fault。嗯,这个坑我在移植到新平台时踩过,后来学乖了,跳转前先检查页表映射范围。

我的调试习惯:ret 指令前加个 hlt,然后用调试器读 %rax%rsp%cr3 三个寄存器的值。确认它们都符合预期,再继续跑。这比出问题了再回头查快得多。

4.7 小结

decompress_kernel()start_kernel(),这中间虽然只有几十条汇编指令,但每一步都关系到系统能不能正常启动。说白了,这就是从“汇编世界”到“C 世界”的桥梁。桥搭好了,后面的事就顺了;桥没搭好,后面再怎么折腾也是白搭。

我个人觉得,理解这个交接过程,是掌握内核启动流程的关键。你想想看,整个内核成百上千万行代码,起点就是这里。从这里开始,C 语言才真正接管了系统。


专注资料整理