本文基于 Linux 5.15.119 实际源码,从 GRUB 加载到第一个用户进程
/sbin/init启动,完整梳理 x86_64 架构的内核启动流程。
目录
- [0. bzImage 的物理结构](#0. bzImage 的物理结构)
- [1. GRUB 加载 bzImage](#1. GRUB 加载 bzImage)
- [2. 实模式(16 位)------ 收集硬件信息](#2. 实模式(16 位)—— 收集硬件信息)
- [3. 切换到保护模式(32 位)](#3. 切换到保护模式(32 位))
- [4. 32 位解压入口](#4. 32 位解压入口)
- [5. 64 位模式下的解压](#5. 64 位模式下的解压)
- [6. 真正的内核 64 位入口](#6. 真正的内核 64 位入口)
- [7. x86 架构的 C 语言入口](#7. x86 架构的 C 语言入口)
- [8. 通用内核初始化 ------ start_kernel()](#8. 通用内核初始化 —— start_kernel())
- [9. rest_init() ------ 创建两个"祖先进程"](#9. rest_init() —— 创建两个"祖先进程")
- [10. kernel_init() ------ 启动第一个用户程序](#10. kernel_init() —— 启动第一个用户程序)
- 总结:完整启动路线图
0. bzImage 的物理结构
你执行 make bzImage 编译出来的文件,其实不是单一文件,而是两部分拼接而成:
┌─────────────────┬─────────────────────────────┐
│ setup.bin │ compressed/vmlinux │
│ (实模式代码) │ (压缩的真正的内核) │
│ ~30KB │ 几MB ~ 几十MB │
└─────────────────┴─────────────────────────────┘
↑ ↑
从 bootsect 开始 从 startup_32 开始
Bootloader(GRUB)把它加载到内存后,CPU 先执行左边的 setup.bin(还是古老的 16 位实模式),然后才进入右边的压缩内核。
拼接工作在
arch/x86/boot/tools/build.c中完成。
1. GRUB 加载 bzImage
GRUB 读取内核文件开头的 内核头部(Kernel Header),获取启动协议信息。
源码位置 :arch/x86/boot/header.S
这个文件开头定义了一个兼容 MS-DOS 的头部(MZ/PE 头),让 GRUB 能识别它:
asm
#ifdef CONFIG_EFI_STUB
.word MZ_MAGIC # "MZ" 魔数
#endif
头部中包含了 GRUB 需要的关键字段:
| 字段 | 含义 |
|---|---|
setup_sects |
setup 部分占多少扇区 |
syssize |
内核大小 |
version |
启动协议版本 |
cmd_line_ptr |
命令行参数地址 |
code32_start |
32 位代码入口点 |
GRUB 根据这些信息把 bzImage 加载到内存,然后把控制权交给 header.S 中的入口。
2. 实模式(16 位)------ 收集硬件信息
CPU 当前状态:16 位实模式,只能直接访问 1MB 内存,没有内存保护。
入口函数:main()
源码位置 :arch/x86/boot/main.c
这是整个内核启动的第一个 C 函数:
c
void main(void)
{
/* 把 header 里的信息复制到 boot_params */
copy_boot_params();
/* 初始化早期控制台 */
console_init();
/* 初始化堆内存 */
init_heap();
/* 检查 CPU 是否支持运行这个内核 */
if (validate_cpu()) {
puts("Unable to boot...");
die();
}
/* 告诉 BIOS 我们要进 64 位模式 */
set_bios_mode();
/* 用 BIOS 中断探测内存布局 */
detect_memory();
/* 设置键盘 */
keyboard_init();
/* 查询各种 BIOS 信息 */
query_ist();
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
query_apm_bios();
#endif
#if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
query_edd();
#endif
/* 设置显示模式 */
set_video();
/* ⭐ 进入保护模式 */
go_to_protected_mode();
}
这些代码看起来古老,但非常关键------它用 BIOS 中断(int 0x10、int 0x15 等)收集硬件信息,全部存进全局变量 boot_params,后面内核会一直用它。
3. 切换到保护模式(32 位)
go_to_protected_mode() 定义在 arch/x86/boot/pm.c:
c
void go_to_protected_mode(void)
{
/* 关闭中断,执行实模式最后的钩子 */
realmode_switch_hook();
/* 开启 A20 地址线(历史遗留,为了访问 1MB 以上内存) */
if (enable_a20()) {
puts("A20 gate not responding, unable to boot...\n");
die();
}
/* 重置协处理器 */
reset_coprocessor();
/* 屏蔽所有中断 */
mask_all_interrupts();
/* 设置 IDT 和 GDT */
setup_idt();
setup_gdt();
/* ⭐ 真正跳转到保护模式 */
protected_mode_jump(boot_params.hdr.code32_start,
(u32)&boot_params + (ds() << 4));
}
真正的切换发生在汇编文件 pmjump.S 中:
asm
SYM_FUNC_START(protected_mode_jump)
movl %edx, %esi # %edx = boot_params 指针
movl %cr0, %edx
orb $X86_CR0_PE, %dl # 设置 CR0 的 PE (Protection Enable) 位
movl %edx, %cr0 # 打开保护模式!
# 长跳转到 32 位代码段
.byte 0x66, 0xea # ljmpl 指令
.long .Lin_pm32 # 目标偏移
.word __BOOT_CS # 代码段选择子
.Lin_pm32:
# 设置 32 位平坦模式的数据段
movl %ecx, %ds
movl %ecx, %es
movl %ecx, %fs
movl %ecx, %gs
movl %ecx, %ss
jmpl *%eax # 跳转到 32 位入口点
发生了什么 :CPU 从 16 位实模式 → 32 位保护模式。现在能访问 4GB 内存了,但还不是 64 位。
4. 32 位解压入口
现在 CPU 开始执行 arch/x86/boot/compressed/head_64.S 中的 startup_32。
注意:文件名虽然是
head_64.S,但前面一大段是.code32(32 位代码)。
asm
.code32
SYM_FUNC_START(startup_32)
cld
cli # 关中断
# 计算"实际加载地址"与"编译时地址"的偏差
# 因为 bootloader 可能把内核加载到了意料之外的位置
leal (BP_scratch+4)(%esi), %esp
call 1f
1: popl %ebp
subl $ rva(1b), %ebp
# 加载新的 GDT
leal rva(gdt)(%ebp), %eax
movl %eax, 2(%eax)
lgdt (%eax)
# 设置数据段寄存器
movl $__BOOT_DS, %eax
movl %eax, %ds
movl %eax, %es
movl %eax, %fs
movl %eax, %gs
movl %eax, %ss
然后它继续:
- 建立临时的 4GB 页表(为进入长模式做准备)
- 打开 PAE (Physical Address Extension,
cr4 |= X86_CR4_PAE) - 设置 EFER.LME(Long Mode Enable)
- 启用分页 (设置
cr0.PG = 1),CPU 自动进入兼容模式 - 长跳转到 64 位代码
5. 64 位模式下的解压
继续在同一份 head_64.S 中,现在 CPU 运行在 64 位模式了。
这段代码的任务是:
- 找到压缩的内核数据(
piggy.o里包着vmlinux.bin.gz或.xz等) - 把它解压到最终运行地址
- 跳转到解压后的内核入口
解压完成后,它跳转到真正的内核入口------startup_64(注意:这是另一个文件里的同名符号!)。
相关文件:
arch/x86/boot/compressed/head_64.Sarch/x86/boot/compressed/misc.c(解压算法的 C 实现)arch/x86/boot/compressed/piggy.S(把压缩后的内核二进制打包成汇编对象)
6. 真正的内核 64 位入口
现在进入 arch/x86/kernel/head_64.S 中的 startup_64。
这是编译后的
vmlinux的第一条指令 ,也是内核真正开始建立自己世界的地方。
asm
.code64
SYM_CODE_START_NOALIGN(startup_64)
/* 设置栈(用 init_task 的栈) */
leaq (__end_init_task - FRAME_SIZE)(%rip), %rsp
/* 调用 startup_64_setup_env 和 verify_cpu */
leaq _text(%rip), %rdi
pushq %rsi
call startup_64_setup_env
popq %rsi
call verify_cpu
/* 修正页表(内核可能被加载到了与编译地址不同的位置) */
leaq _text(%rip), %rdi
pushq %rsi
call __startup_64
popq %rsi
/* 形成 CR3 值(页表基址) */
addq $(early_top_pgt - __START_KERNEL_map), %rax
/* 设置 CR4(PAE + PGE) */
movl $(X86_CR4_PAE | X86_CR4_PGE), %ecx
movq %rcx, %cr4
/* 切换到新的页表 */
movq %rax, %cr3
/* 加载内核的 GDT */
lgdt early_gdt_descr(%rip)
/* 清空数据段寄存器 */
xorl %eax, %eax
movl %eax, %ds
movl %eax, %ss
movl %eax, %es
movl %eax, %fs
movl %eax, %gs
/* 设置 IDT(早期中断描述符表) */
pushq %rsi
call early_setup_idt
popq %rsi
/* 设置 EFER(使能 SYSCALL 等) */
movl $MSR_EFER, %ecx
rdmsr
btsl $_EFER_SCE, %eax /* Enable System Call */
...
wrmsr
/* 设置 CR0 */
movl $CR0_STATE, %eax
movq %rax, %cr0
/* 清空中断标志 */
pushq $0
popfq
/* 准备跳转到 C 代码 */
movq %rsi, %rdi # real_mode_data 作为参数
.Ljump_to_C_code:
xorl %ebp, %ebp # 清空帧指针
movq initial_code(%rip), %rax # rax = x86_64_start_kernel
pushq $__KERNEL_CS
pushq %rax
lretq # 远返回,进入 x86_64_start_kernel
关键点:
initial_code是一个全局变量,值为x86_64_start_kernelinitial_stack指向init_thread_union(这是 PID 0,也就是 swapper/idle 进程的栈)lretq完成一次远返回,同时切换代码段和跳转到目标地址
7. x86 架构的 C 语言入口
lretq 把你带到了 arch/x86/kernel/head64.c:
c
asmlinkage __visible void __init x86_64_start_kernel(char * real_mode_data)
{
/* 编译期检查 */
BUILD_BUG_ON(MODULES_VADDR < __START_KERNEL_map);
...
/* 初始化 CR4 影子寄存器 */
cr4_init_shadow();
/* 清除临时页表(去掉 identity map) */
reset_early_page_tables();
/* 清零 BSS 段 */
clear_bss();
/* 复制 boot_params 到内核空间 */
copy_bootdata(__va(real_mode_data));
/* 加载 CPU 微码 */
load_ucode_bsp();
/* 设置正式页表的高地址映射 */
init_top_pgt[511] = early_top_pgt[511];
x86_64_start_reservations(real_mode_data);
}
void __init x86_64_start_reservations(char *real_mode_data)
{
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(); /* ⭐ 进入体系无关的通用初始化! */
}
从这里开始,代码变得体系无关 了------不管是 x86、ARM 还是 RISC-V,最终都会调用 start_kernel()。
8. 通用内核初始化 ------ start_kernel()
这是整个内核最著名的函数,位于 init/main.c:
c
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
char *after_dashes;
/* 设置 init_task(PID 0)的栈结束魔数 */
set_task_stack_end_magic(&init_task);
smp_setup_processor_id();
cgroup_init_early();
/* 关中断,初始化期间不允许打扰 */
local_irq_disable();
early_boot_irqs_disabled = true;
boot_cpu_init();
page_address_init();
/* 打印 "Linux version 5.15.119 ..." */
pr_notice("%s", linux_banner);
early_security_init();
/* ⭐ 架构相关初始化(内存、CPU、ACPI 等) */
setup_arch(&command_line);
setup_boot_config();
setup_command_line(command_line);
setup_nr_cpu_ids();
setup_per_cpu_areas();
smp_prepare_boot_cpu();
boot_cpu_hotplug_init();
/* 建立内存管理区 */
build_all_zonelists(NULL);
page_alloc_init();
/* 打印命令行 */
pr_notice("Kernel command line: %s\n", saved_command_line);
/* 解析早期参数(如 mem=, console=) */
parse_early_param();
parse_args("Booting kernel", static_command_line, ...);
/* ⭐ 初始化内存管理 */
mm_init();
ftrace_init();
sched_init(); /* ⭐ 初始化调度器 */
radix_tree_init();
workqueue_init_early();
rcu_init();
trace_init();
context_tracking_init();
early_irq_init();
init_IRQ(); /* 初始化中断 */
tick_init();
init_timers();
timekeeping_init();
time_init(); /* 初始化时间子系统 */
random_init(command_line);
boot_init_stack_canary();
/* 开中断 */
local_irq_enable();
early_boot_irqs_disabled = false;
console_init(); /* 正式初始化控制台 */
/* ⭐ 初始化 VFS */
vfs_caches_init();
signals_init();
pagecache_init();
proc_root_init(); /* 创建 /proc */
nsfs_init();
cgroup_init();
...
check_bugs(); /* 检查 CPU bug */
/* ⭐ 调用 rest_init() */
arch_call_rest_init();
}
start_kernel() 初始化了内核的几乎所有核心子系统:
- 内存管理(MM)
- 进程调度
- 中断、定时器
- 文件系统缓存
- procfs、cgroups
- ...
但最后它自己不变成用户进程,而是调用 rest_init()。
9. rest_init() ------ 创建两个"祖先进程"
源码位置 :init/main.c
c
noinline void __ref rest_init(void)
{
struct task_struct *tsk;
int pid;
rcu_scheduler_starting();
/*
* 创建 PID 1:init 进程(用户态的祖先)
*/
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
/* 把 init 固定在 boot CPU 上 */
rcu_read_lock();
tsk = find_task_by_pid_ns(pid, &init_pid_ns);
tsk->flags |= PF_NO_SETAFFINITY;
set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
rcu_read_unlock();
/*
* 创建 PID 2:kthreadd(内核线程的祖先)
*/
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
rcu_read_lock();
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
rcu_read_unlock();
/*
* 启用调度器检查
*/
system_state = SYSTEM_SCHEDULING;
complete(&kthreadd_done);
/*
* 当前线程(PID 0)变成 idle 进程,永远执行 schedule()
*/
schedule_preempt_disabled();
cpu_startup_entry(CPUHP_ONLINE); /* 进入 idle 循环 */
}
这是 Linux 进程树的起点:
| PID | 进程 | 作用 |
|---|---|---|
| 0 | swapper/idle | 当前线程变身而来,没事干时执行它 |
| 1 | kernel_init | 接下来启动第一个用户程序(/sbin/init) |
| 2 | kthreadd | 专门"收养"内核线程,所有内核线程的父进程 |
10. kernel_init() ------ 启动第一个用户程序
源码位置 :init/main.c
c
static int __ref kernel_init(void *unused)
{
/* 等 kthreadd 准备好 */
wait_for_completion(&kthreadd_done);
/* 加载真正的根文件系统 */
kernel_init_freeable();
/* 等所有异步 __init 代码完成 */
async_synchronize_full();
/* 释放 __init 段内存(启动完成后这些代码不再需要) */
free_initmem();
mark_readonly();
pti_finalize();
/* 系统正式运行 */
system_state = SYSTEM_RUNNING;
numa_default_policy();
rcu_end_inkernel_boot();
/* 按优先级尝试启动第一个用户进程 */
/* 1. initrd 中的 /init */
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
}
/* 2. 命令行 init=xxx 指定的程序 */
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
}
/* 3. 编译时默认的 init */
if (CONFIG_DEFAULT_INIT[0] != '\0') {
ret = run_init_process(CONFIG_DEFAULT_INIT);
if (!ret)
return 0;
}
/* 4. 默认路径 */
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
/* 全都失败 */
panic("No working init found. Try passing init= option to kernel.");
}
一旦 run_init_process("/sbin/init") 成功,内核就完成了它的启动使命。
从这一刻起,系统控制权交给了用户空间。
你看到的 systemd、sysvinit、或 bash,就是从这里开始的。
总结:完整启动路线图
Mermaid 流程图
GRUB/Bootloader 加载 bzImage
arch/x86/boot/header.S 内核头部
arch/x86/boot/main.c main()
收集硬件信息 copy_boot_params / detect_memory
arch/x86/boot/pm.c + pmjump.S go_to_protected_mode()
arch/x86/boot/compressed/head_64.S startup_32/startup_64 建立页表 + 解压内核
arch/x86/kernel/head_64.S startup_64 设置页表/GDT/IDT
arch/x86/kernel/head64.c x86_64_start_kernel()
init/main.c start_kernel() 初始化所有子系统
rest_init()
PID 0 cpu_startup_entry() swapper/idle
PID 1 kernel_init() 用户态祖先
PID 2 kthreadd 内核线程祖先
run_init_process() /sbin/init /etc/init /bin/init /bin/sh
用户空间 systemd / init / bash
ASCII 路线图(备用)
GRUB 加载 bzImage
│
▼
┌─────────────────────────────────────────────┐
│ arch/x86/boot/header.S │ 内核头部,存放启动协议信息
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ arch/x86/boot/main.c main() │ ◄── 16位实模式
│ └── go_to_protected_mode() │ 收集硬件信息到 boot_params
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ arch/x86/boot/pmjump.S │ ◄── 打开 CR0.PE
│ protected_mode_jump() │ 进入 32 位保护模式
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ arch/x86/boot/compressed/head_64.S │ ◄── 32→64位切换
│ startup_32 → startup_64 │ 建立页表,解压内核
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ arch/x86/kernel/head_64.S │ ◄── 真正的内核 64 位入口
│ startup_64 → lretq │ 设置页表/GDT/IDT
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ arch/x86/kernel/head64.c │ ◄── x86 架构 C 入口
│ x86_64_start_kernel() │ 清零 BSS,加载微码
│ └── start_kernel() │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ init/main.c start_kernel() │ ◄── 初始化所有子系统
│ └── arch_call_rest_init() │ MM、调度、IRQ、VFS...
│ └── rest_init() │
│ ├── kernel_thread(kernel_init) │ PID 1
│ ├── kernel_thread(kthreadd) │ PID 2
│ └── cpu_startup_entry() │ PID 0 (idle)
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ init/main.c kernel_init() │ ◄── 启动 /sbin/init
│ └── run_init_process() │
└─────────────────────────────────────────────┘
│
▼
用户空间(systemd / init / bash)