从事嵌入式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
,里面有这么一段代码:
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
到底是什么?它指向哪里?让我们在源码中寻找答案。
在同一个文件中,我们可以找到 initial_code
的定义:
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
的定义:
1 | asmlinkage __visible void __init __noreturn x86_64_start_kernel(char *real_mode_data) |
这个函数是内核启动的关键转折点,主要解决以下几个核心问题:
内存安全性验证:这些BUILD_BUG_ON
检查确保内核在编译时就能发现内存布局问题,避免运行时的灾难性错误。
页表管理的准确性:此时需要从早期的简单映射切换到正式的虚拟内存管理。5级页表是Intel在Skylake-X架构引入的特性,支持128PB的虚拟地址空间,主要用于大型数据中心和高性能计算场景。
安全功能的时序要求:SME(内存加密)、KASAN(地址检测)、TDX(可信执行)等安全功能有严格的初始化顺序,必须在特定时机启用以确保整个系统的安全基础。
硬件兼容性:不同的CPU特性(如微码更新、中断处理)需要在合适的时机初始化,确保内核能在各种硬件平台上正确运行。
做完这些准备工作后,最后调用了x86_64_start_reservations
函数:
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()
函数的定义:
1 | void start_kernel(void) |
你可以看到,这个函数做了很多初始化工作,比如 boot_cpu_init()
是在初始化启动CPU,page_address_init()
初始化页地址映射等等,当然,这里只是展示了部分代码,start_kernel()
函数非常长,感兴趣的小伙伴可以自行查看源码,这里如果展开全部讲,我们可能需要写一本书了,哈哈。
所以,到这里我们就明白了,Linux内核的启动流程大致如下:
1 | 1. 汇编代码初始化 (header_64.S) |
本文只是对Linux内核启动流程的一个初步探讨,内核源码非常庞大且复杂,如果一开始就不放过每一个细节,会陷入无尽的细节中无法抽身,所以我们先从整体上把握大致流程,一步一步深入,希望对大家有所帮助。如果你对某个具体的初始化步骤感兴趣,可以继续深入研究相关代码