从事嵌入式Linux这么久,一直在dts、驱动、应用等方面打转,对Linux的源码了解甚少。最近决定深入内核源码,就从最基础的问题开始:Linux内核是怎么启动的?它的主入口在哪里?
由于Linux内核是在持续更新中的,本文分析源码时内核版本为6.16.7,其实也大差不差的。
启动流程概览:从硬件到软件的过渡
很多人可能以为Linux内核启动就像普通程序一样,直接从main函数开始。但实际上,在进入到C语言编写的main.c文件之前,系统必须先通过汇编代码完成最基础的硬件初始化工作。
这是因为此时硬件环境还没有准备好运行C语言代码。以x86架构为例,内核的入口文件是arch/x86/boot/header_32.S(32位)或header_64.S(64位),该文件完成了最初的硬件初始化工作,设置了段寄存器,启用了分页等,然后跳转到C语言编写的main.c文件,这里我们挑选header_64.S,里面有这么一段代码:(在第415行)
1 | .Ljump_to_C_code: |
这段代码的关键在于第三行的 callq *initial_code(%rip)让我们逐行分析:
xorl %ebp, %ebp- 清零帧指针寄存器,为进入C函数做准备ANNOTATE_RETPOLINE_SAFE- 这是一个安全标记,用于防御侧信道攻击callq *initial_code(%rip)- 这是最关键的一行,它通过间接调用跳转到initial_code变量所指向的地址ud2- 未定义指令,如果程序执行到这里说明出现了错误
initial_code 的定义就在同一个文件里,第479行:
1 | /* Both SMP bootup and ACPI suspend change these variables */ |
简单来说,这行代码的意思是:定义一个名为 initial_code 的8字节变量,它的值是 x86_64_start_kernel 函数的地址。
从汇编到C语言的桥梁
x86_64_start_kernel 不在当前的汇编文件里,而是在 arch/x86/kernel/head_64.c 中,开头注释直接说明了它的职责:
1 | // SPDX-License-Identifier: GPL-2.0 |
这是个C语言中间层,为进入真正的内核初始化做准备。直接找到 x86_64_start_kernel 的定义(第291行):
1 | void __init __noreturn x86_64_start_reservations(char *real_mode_data) |
虽然代码看起来挺复杂,但对于我们理解内核启动流程来说,最重要的就是最后一行 start_kernel(),这个函数才是真正的内核主入口点,接下来我们就来到了Linux内核的大门口main.c文件了。
内核的真正入口:main.c
main.c文件位于init/main.c,这是内核初始化的核心文件,包含了内核启动时的各种初始化工作,那么直接看start_kernel()函数的定义(第898行):
1 | void start_kernel(void) |
你可以看到,这个函数做了很多初始化工作,比如 boot_cpu_init() 是在初始化启动CPU,page_address_init() 初始化页地址映射等等,当然,这里只是展示了部分代码,start_kernel() 函数非常长,感兴趣的小伙伴可以自行查看源码,这里如果展开全部讲,我们可能需要写一本书了,哈哈。
到这里整条链路就清晰了:header_64.S 完成最基础的硬件初始化,通过 initial_code 变量跳进 x86_64_start_kernel,再经 x86_64_start_reservations 最终落到 start_kernel()。
内核源码庞大,start_kernel() 本身就几百行,每一个初始化函数背后都是另一个兔子洞。这篇只把最外层的调用链走了一遍——知道”从哪里进来”比”每一步干了什么”重要,后者可以一个函数一个函数慢慢啃。