大西冰城的博客

Linux内核启动探秘:从汇编到C语言的完整调用链路

Word count: 2.5kReading time: 9 min
2025/09/12
loading

从事嵌入式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
2
3
4
5
.Ljump_to_C_code:
xorl %ebp, %ebp # clear frame pointer
ANNOTATE_RETPOLINE_SAFE
callq *initial_code(%rip) #这里很关键
ud2

这段代码的关键在于第三行的 callq *initial_code(%rip)让我们逐行分析:

  • xorl %ebp, %ebp - 清零帧指针寄存器,为进入C函数做准备
  • ANNOTATE_RETPOLINE_SAFE - 这是一个安全标记,用于防御侧信道攻击
  • callq *initial_code(%rip) - 这是最关键的一行,它通过间接调用跳转到 initial_code 变量所指向的地址
  • ud2 - 未定义指令,如果程序执行到这里说明出现了错误

那么问题来了,这个神秘的 initial_code 到底是什么?它指向哪里?让我们在源码中寻找答案。
在同一个文件中,我们可以找到 initial_code 的定义:

1
2
3
4
    	/* Both SMP bootup and ACPI suspend change these variables */
__REFDATA
.balign 8
SYM_DATA(initial_code, .quad x86_64_start_kernel) # 指向架构相关的启动函数

简单来说,这行代码的意思是:定义一个名为 initial_code 的8字节变量,它的值是 x86_64_start_kernel 函数的地址。

从汇编到C语言的桥梁

那么这个 x86_64_start_kernel又是什么呢,它在哪里呢?当然,不在当前的汇编文件中,而是在 arch/x86/kernel/head_64.c 文件中,这个文件是干什么的呢?其实在开头的注释中已经说明了:

1
2
3
4
5
6
7
8
// SPDX-License-Identifier: GPL-2.0
/*
* prepare to run common code
* 翻译: 准备运行通用代码
* Copyright (C) 2000 Andrea Arcangeli <andrea@suse.de> SuSE
*/

/* cpu_feature_enabled() cannot be used this early */

这是个C语言中间文件,它是为运行通用代码做准备的中间层,为进入到真正的内核初始化做准备,好了,废话不多说,我们直接找到x86_64_start_kernel的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
asmlinkage __visible void __init __noreturn x86_64_start_kernel(char *real_mode_data)
{
/*
* Build-time sanity checks on the kernel image and module
* area mappings. (these are purely build-time and produce no code)
*
* 翻译: 对内核映像和模块区域映射进行编译时的健全性检查。(这些纯粹是编译时的,不会生成代码)
*/
BUILD_BUG_ON(MODULES_VADDR < __START_KERNEL_map); // 模块虚拟地址必须大于内核映射起始地址
BUILD_BUG_ON(MODULES_VADDR - __START_KERNEL_map < KERNEL_IMAGE_SIZE); // 模块虚拟地址与内核映射起始地址的差值必须大于内核映像大小
BUILD_BUG_ON(MODULES_LEN + KERNEL_IMAGE_SIZE > 2 * PUD_SIZE); // 模块长度加内核映像大小必须小于等于2个PUD大小
BUILD_BUG_ON((__START_KERNEL_map & ~PMD_MASK) != 0); // 内核映射起始地址必须与PMD掩码对齐
BUILD_BUG_ON((MODULES_VADDR & ~PMD_MASK) != 0); // 模块虚拟地址必须与PMD掩码对齐
BUILD_BUG_ON(!(MODULES_VADDR > __START_KERNEL)); // 模块虚拟地址必须大于内核起始地址
MAYBE_BUILD_BUG_ON(!(((MODULES_END - 1) & PGDIR_MASK) ==
(__START_KERNEL & PGDIR_MASK))); // 模块结束地址与内核起始地址必须在同一页目录
BUILD_BUG_ON(__fix_to_virt(__end_of_fixed_addresses) <= MODULES_END); // 固定地址结束的虚拟地址必须大于模块结束地址

cr4_init_shadow(); // 初始化CR4寄存器的影子变量

/* Kill off the identity-map trampoline 翻译: 取消恒等映射的跳板 */
reset_early_page_tables(); // 重置早期页表,取消恒等映射

if (pgtable_l5_enabled())
{ // 如果启用了5级页表
page_offset_base = __PAGE_OFFSET_BASE_L5; // 设置5级页表的页面偏移基址
vmalloc_base = __VMALLOC_BASE_L5; // 设置5级页表的虚拟内存分配基址
vmemmap_base = __VMEMMAP_BASE_L5; // 设置5级页表的虚拟内存映射基址
}

clear_bss(); // 清理BSS段

/*
* This needs to happen *before* kasan_early_init() because latter maps stuff
* into that page.
*
* 翻译: 这需要在kasan_early_init()之前完成,因为后者会映射一些东西到该页面。
*/
clear_page(init_top_pgt); // 清零初始化的顶级页表

/*
* SME support may update early_pmd_flags to include the memory
* encryption mask, so it needs to be called before anything
* that may generate a page fault.
*
* 翻译: SME支持可能会更新early_pmd_flags以包含内存加密掩码,因此需要在可能生成页面错误的任何操作之前调用它。
*/
sme_early_init(); // 早期初始化内存加密支持

kasan_early_init(); // 早期初始化内核地址空间布局随机化

/*
* Flush global TLB entries which could be left over from the trampoline page
* table.
*
* This needs to happen *after* kasan_early_init() as KASAN-enabled .configs
* instrument native_write_cr4() so KASAN must be initialized for that
* instrumentation to work.
*
* 翻译: 刷新可能遗留在跳板页表中的全局TLB条目。
* 这需要在kasan_early_init()之后进行,因为启用KASAN的.configs
* 会对native_write_cr4()进行插装,因此必须初始化KASAN才能使该插装工作。
*/
__native_tlb_flush_global(this_cpu_read(cpu_tlbstate.cr4)); // 刷新全局TLB条目

idt_setup_early_handler(); // 设置早期的中断描述符表处理程序

/* Needed before cc_platform_has() can be used for TDX */
tdx_early_init(); // 早期初始化可信执行环境

copy_bootdata(__va(real_mode_data)); // 复制实模式数据到内核虚拟地址空间

/*
* Load microcode early on BSP.
* 翻译: 在BSP上尽早加载微码。
*/
load_ucode_bsp();

/* set init_top_pgt kernel high mapping*/
init_top_pgt[511] = early_top_pgt[511]; // 设置内核高映射的顶级页表项

x86_64_start_reservations(real_mode_data); // 进入内核预留资源初始化函数
}

这个函数是内核启动的关键转折点,主要解决以下几个核心问题:

内存安全性验证:这些BUILD_BUG_ON检查确保内核在编译时就能发现内存布局问题,避免运行时的灾难性错误。

页表管理的准确性:此时需要从早期的简单映射切换到正式的虚拟内存管理。5级页表是Intel在Skylake-X架构引入的特性,支持128PB的虚拟地址空间,主要用于大型数据中心和高性能计算场景。

安全功能的时序要求:SME(内存加密)、KASAN(地址检测)、TDX(可信执行)等安全功能有严格的初始化顺序,必须在特定时机启用以确保整个系统的安全基础。

硬件兼容性:不同的CPU特性(如微码更新、中断处理)需要在合适的时机初始化,确保内核能在各种硬件平台上正确运行。

做完这些准备工作后,最后调用了x86_64_start_reservations函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void __init __noreturn x86_64_start_reservations(char *real_mode_data)
{
/* version is always not zero if it is copied */
if (!boot_params.hdr.version) // 检查引导参数版本
copy_bootdata(__va(real_mode_data)); // 复制引导数据

x86_early_init_platform_quirks(); // 处理平台特性

switch (boot_params.hdr.hardware_subarch) // 根据硬件子架构进行不同的初始化
{
case X86_SUBARCH_INTEL_MID: // Intel MID平台
x86_intel_mid_early_setup(); // 进行Intel MID的早期设置
break;
default:
break;
}

start_kernel(); // 看这里,终于进入到真正的内核初始化函数了
}

虽然代码看起来挺复杂,但对于我们理解内核启动流程来说,最重要的就是最后一行 start_kernel(),这个函数才是真正的内核主入口点,接下来我们就来到了Linux内核的大门口main.c文件了。

内核的真正入口:main.c

main.c文件位于init/main.c,这是内核初始化的核心文件,包含了内核启动时的各种初始化工作,那么直接看start_kernel()函数的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void start_kernel(void)
{
char *command_line;
char *after_dashes;

set_task_stack_end_magic(&init_task); // 设置栈结束标记
smp_setup_processor_id(); // 设置处理器ID
debug_objects_early_init(); // 调试对象初始化
init_vmlinux_build_id(); // 初始化内核构建ID

cgroup_init_early(); // 早期控制组初始化

local_irq_disable(); // 禁用本地中断
early_boot_irqs_disabled = true; // 标记早期中断已禁用

/*
* Interrupts are still disabled. Do necessary setups, then
* enable them.
* 翻译: 中断仍然被禁用。进行必要的设置,然后启用它们。
*/
boot_cpu_init(); // 启动CPU初始化
page_address_init(); // 页地址初始化
pr_notice("%s", linux_banner); // 打印内核启动横幅
setup_arch(&command_line); // 架构相关的设置
/* Static keys and static calls are needed by LSMs */
jump_label_init(); // 跳转标签初始化

/** ... 省略部分代码 ... **/
/* Do the rest non-__init'ed, we're now alive 翻译: 其余部分不再是__init',终于活过来了 */
rest_init(); // 剩余初始化

/*
* Avoid stack canaries in callers of boot_init_stack_canary for gcc-10
* and older.
*
* 翻译: 避免在gcc-10及更早版本的boot_init_stack_canary调用者中使用栈保护
*/
#if !__has_attribute(__no_stack_protector__) // 如果编译器不支持no_stack_protector属性
prevent_tail_call_optimization(); // 防止尾调用优化
#endif
}

你可以看到,这个函数做了很多初始化工作,比如 boot_cpu_init() 是在初始化启动CPU,page_address_init() 初始化页地址映射等等,当然,这里只是展示了部分代码,start_kernel() 函数非常长,感兴趣的小伙伴可以自行查看源码,这里如果展开全部讲,我们可能需要写一本书了,哈哈。

所以,到这里我们就明白了,Linux内核的启动流程大致如下:

1
2
3
4
5
6
7
1. 汇编代码初始化 (header_64.S)

2. 跳转到C语言中间层 (x86_64_start_kernel)

3. 进入内核主入口 (start_kernel)

4. 各种初始化

本文只是对Linux内核启动流程的一个初步探讨,内核源码非常庞大且复杂,如果一开始就不放过每一个细节,会陷入无尽的细节中无法抽身,所以我们先从整体上把握大致流程,一步一步深入,希望对大家有所帮助。如果你对某个具体的初始化步骤感兴趣,可以继续深入研究相关代码

CATALOG
  1. 1. 启动流程概览:从硬件到软件的过渡
  2. 2. 从汇编到C语言的桥梁
  3. 3. 内核的真正入口:main.c