Linux内核并没有像普通C语言用户态程序那样定义一个main函数作为执行入口。内核是一个独立于标准C运行时的裸机程序映像,它的起点由体系结构相关的引导代码和链接脚本共同决定。当 bootloader 把内核加载进内存并把 CPU 控制权交出来时,处理器直接跳到内核映像中特定的汇编入口符号,随后才一步步过渡到 C 语言环境。

为什么内核没有main函数
在用户态,C 程序经过编译器、链接器配合 libc 的启动 routines(如 _start)最终调用我们写的 main 函数。这套机制依赖操作系统加载器设置好栈、参数 argc/argv 以及标准输入输出。而 Linux 内核本身就是操作系统,它运行在硬件之上,没有更底层的系统来给它传递这些运行环境,因此不可能沿用 main 的约定。
内核的构建系统通过链接脚本(如 arch/x86/kernel/vmlinux.lds.S)显式安排入口符号,例如 x86 下的 startup_32 或 startup_64。这些符号是纯汇编例程,负责关闭中断、设置早期页表、切换运行级别,并把内核自身的 bss 段清零。等基础环境就绪,才调用用 C 实现的高层初始化函数。也就是说,内核用体系结构相关的“入口汇编”替代了 libc 中的 _start 加 main 的组合。
内核的启动主线:从汇编到start_kernel
以 x86_64 为例,上电或复位后 firmware 加载 bootloader,bootloader 读取内核头并把控制权交给内核的 head_64.S 中的 startup_64。这段代码先做身份映射页表,再跳入 C 前的准备函数。压缩内核还会先执行自解压代码,解压后跳转至真正内核的入口。整个流程最终汇聚到 init/main.c 里的 start_kernel 函数。
start_kernel 是内核所有子系统初始化的统一交汇点,它相当于内核世界的“main”。在这个函数里会依次调用 setup_arch、sched_init、init_IRQ、rest_init 等例程。rest_init 中会通过 kernel_thread 创建一号进程(init),而 0 号进程就是当前运行 start_kernel 的上下文,之后演变为 idle 线程。下面是一段简化的逻辑示意:
// 伪代码:描述 start_kernel 的核心调用脉络
void start_kernel(void)
{
setup_arch(&command_line); // 体系结构相关初始化
sched_init(); // 调度器早期初始化
init_IRQ(); // 中断控制器与向量表
time_init(); // 时钟源初始化
rest_init(); // 创建 init 进程与 kthreadd
}
static noinline void rest_init(void)
{
// 创建 PID 1 的 init 进程
kernel_thread(kernel_init, NULL, CLONE_FS);
// 当前 cpu 进入 idle 循环
cpu_startup_entry(CPUHP_ONLINE);
}
不同架构的入口差异
虽然最终都走到 start_kernel,但各 CPU 架构的“前戏”完全不同。ARM32 的入口通常是 stext,定义在 arch/arm/kernel/head.S,它假设 MMU 关闭、CPU 处于特权模式,由 bootloader 把设备树指针放在指定寄存器。RISC-V 的入口则是 _start,位于 arch/riscv/kernel/head.S,负责设置 trap 向量并启用早期页表。这些汇编文件通过链接脚本保证被放在内核映像最前面。
下面的表格列出几种常见架构的 early 入口符号与负责文件,帮助建立直观印象:
| 架构 | 入口符号 | 主要汇编文件 |
|---|---|---|
| x86_64 | startup_64 | arch/x86/boot/compressed/head_64.S |
| ARM32 | stext | arch/arm/kernel/head.S |
| ARM64 | _text / __primary_switched | arch/arm64/kernel/head.S |
| RISC-V | _start | arch/riscv/kernel/head.S |
理解这些差异后就会明白,所谓“内核没有 main”只是因为入口名字和形式变了,而不是内核不需要一个统管初始化的主函数。start_kernel 就是那个事实上的 main,只是它不被 C 运行时自动调用,而是由架构代码手工跳入。
如何从源码验证这一点
如果想亲手确认,可以下载对应版本的内核源码,打开 init/main.c 搜索 start_kernel,再结合自己平台的 head.S 看调用链。在链接脚本里搜索 ENTRY 指令,例如 x86 的 vmlinux.lds.S 中通常写有 ENTRY(startup_64),这明确告诉链接器入口点。你也可以用 objdump 查看生成的内核映像符号表:
# 查看 vmlinux 的入口符号 readelf -h vmlinux | grep Entry # 反汇编头部,确认 startup_64 位置 objdump -d vmlinux | grep -A20 "<startup_64>:"
通过这些手段,能清晰看到内核映像的入口并不是 main,而是一个架构相关的汇编标签,之后才进入 C 写的 start_kernel。对于阅读内核源码的人来说,把 start_kernel 当作逻辑起点,顺着它的调用树去理解内存、进程、文件系统的诞生过程,是最有效率的学习路径。
小结
Linux 内核没有传统意义上的 main 函数,因为它的运行模型与用户态程序不同:它直接接管硬件,由 bootloader 跳到体系结构指定的汇编入口,完成最早期环境设置后调用 start_kernel。start_kernel 承担了类似 main 的职责,统一拉起内核各子系统。弄清楚这个启动边界,既能避免“找不到 main”的困惑,也能为后续深入分析内核初始化流程打下扎实基础。
linux内核内核启动start_kernel修改时间:2026-08-11 11:12:34