大西冰城的博客

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

Word count: 1.1kReading time: 4 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,里面有这么一段代码:(在第415行)

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 的定义就在同一个文件里,第479行:

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 的定义(第291行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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:
x86_intel_mid_early_setup();
break;
default:
break;
}

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

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

内核的真正入口:main.c

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

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
void start_kernel(void)
{
char *command_line;
char *after_dashes;

set_task_stack_end_magic(&init_task);
smp_setup_processor_id();
debug_objects_early_init();
init_vmlinux_build_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();
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 */
rest_init();

/*
* Avoid stack canaries in callers of boot_init_stack_canary for gcc-10
* and older.
*/
#if !__has_attribute(__no_stack_protector__)
prevent_tail_call_optimization();
#endif
}

你可以看到,这个函数做了很多初始化工作,比如 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() 本身就几百行,每一个初始化函数背后都是另一个兔子洞。这篇只把最外层的调用链走了一遍——知道”从哪里进来”比”每一步干了什么”重要,后者可以一个函数一个函数慢慢啃。

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