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 世界前的最后准备
跳过去之前,还有几件事必须做完。我列一下:
- 切换栈:从解压用的临时栈,切到内核自己的栈
- 设置页表:至少保证能访问到内核所在的物理内存
- 清 BSS:把未初始化的全局变量区域清零
- 保存启动信息:比如 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 一张图看懂控制权交接
我画了张流程图,把整个交接过程串起来。你看一眼就明白了:
4.6 避坑指南
我曾经在这个交接点上栽过跟头,分享几个经验:
坑1:栈指针没切干净
解压阶段的栈和内核栈可能在不同的内存区域。如果 %rsp 没切对,进了 start_kernel() 后一压栈就把解压阶段的数据给踩了。我遇到过一回,现象是内核启动到一半莫名其妙死掉,查了两天才发现是栈重叠了。
坑2:页表覆盖范围不够
解压阶段的页表可能只映射了解压缓冲区。如果内核镜像被解压到了页表覆盖范围之外,跳转过去直接 page fault。嗯,这个坑我在移植到新平台时踩过,后来学乖了,跳转前先检查页表映射范围。
我的调试习惯:在 ret 指令前加个 hlt,然后用调试器读 %rax、%rsp、%cr3 三个寄存器的值。确认它们都符合预期,再继续跑。这比出问题了再回头查快得多。
4.7 小结
从 decompress_kernel() 到 start_kernel(),这中间虽然只有几十条汇编指令,但每一步都关系到系统能不能正常启动。说白了,这就是从“汇编世界”到“C 世界”的桥梁。桥搭好了,后面的事就顺了;桥没搭好,后面再怎么折腾也是白搭。
我个人觉得,理解这个交接过程,是掌握内核启动流程的关键。你想想看,整个内核成百上千万行代码,起点就是这里。从这里开始,C 语言才真正接管了系统。