〇、全景:一个 CPU 要经历三次"模式跃迁"才真正跑起内核
一台机器加电后,并不是"啪"地一下就进到 start_kernel()。多核系统里的第一个 CPU(BSP,Bootstrap Processor)要像闯关一样,从最原始的实模式 (16 位、1MB 寻址)一路升级到保护模式 (32 位),再到长模式(64 位),最后才能用 C 语言执行内核的初始化逻辑。这条路的每一步,都是"CPU 当前能干什么"的一次质变。
#mermaid-svg-mWtDMvC50Yo7Py0u{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mWtDMvC50Yo7Py0u .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mWtDMvC50Yo7Py0u .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mWtDMvC50Yo7Py0u .error-icon{fill:#552222;}#mermaid-svg-mWtDMvC50Yo7Py0u .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mWtDMvC50Yo7Py0u .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mWtDMvC50Yo7Py0u .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mWtDMvC50Yo7Py0u .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mWtDMvC50Yo7Py0u .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mWtDMvC50Yo7Py0u .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mWtDMvC50Yo7Py0u .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mWtDMvC50Yo7Py0u .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mWtDMvC50Yo7Py0u .marker.cross{stroke:#333333;}#mermaid-svg-mWtDMvC50Yo7Py0u svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mWtDMvC50Yo7Py0u p{margin:0;}#mermaid-svg-mWtDMvC50Yo7Py0u .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mWtDMvC50Yo7Py0u .cluster-label text{fill:#333;}#mermaid-svg-mWtDMvC50Yo7Py0u .cluster-label span{color:#333;}#mermaid-svg-mWtDMvC50Yo7Py0u .cluster-label span p{background-color:transparent;}#mermaid-svg-mWtDMvC50Yo7Py0u .label text,#mermaid-svg-mWtDMvC50Yo7Py0u span{fill:#333;color:#333;}#mermaid-svg-mWtDMvC50Yo7Py0u .node rect,#mermaid-svg-mWtDMvC50Yo7Py0u .node circle,#mermaid-svg-mWtDMvC50Yo7Py0u .node ellipse,#mermaid-svg-mWtDMvC50Yo7Py0u .node polygon,#mermaid-svg-mWtDMvC50Yo7Py0u .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mWtDMvC50Yo7Py0u .rough-node .label text,#mermaid-svg-mWtDMvC50Yo7Py0u .node .label text,#mermaid-svg-mWtDMvC50Yo7Py0u .image-shape .label,#mermaid-svg-mWtDMvC50Yo7Py0u .icon-shape .label{text-anchor:middle;}#mermaid-svg-mWtDMvC50Yo7Py0u .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mWtDMvC50Yo7Py0u .rough-node .label,#mermaid-svg-mWtDMvC50Yo7Py0u .node .label,#mermaid-svg-mWtDMvC50Yo7Py0u .image-shape .label,#mermaid-svg-mWtDMvC50Yo7Py0u .icon-shape .label{text-align:center;}#mermaid-svg-mWtDMvC50Yo7Py0u .node.clickable{cursor:pointer;}#mermaid-svg-mWtDMvC50Yo7Py0u .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mWtDMvC50Yo7Py0u .arrowheadPath{fill:#333333;}#mermaid-svg-mWtDMvC50Yo7Py0u .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mWtDMvC50Yo7Py0u .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mWtDMvC50Yo7Py0u .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mWtDMvC50Yo7Py0u .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mWtDMvC50Yo7Py0u .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mWtDMvC50Yo7Py0u .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mWtDMvC50Yo7Py0u .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mWtDMvC50Yo7Py0u .cluster text{fill:#333;}#mermaid-svg-mWtDMvC50Yo7Py0u .cluster span{color:#333;}#mermaid-svg-mWtDMvC50Yo7Py0u div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mWtDMvC50Yo7Py0u .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mWtDMvC50Yo7Py0u rect.text{fill:none;stroke-width:0;}#mermaid-svg-mWtDMvC50Yo7Py0u .icon-shape,#mermaid-svg-mWtDMvC50Yo7Py0u .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mWtDMvC50Yo7Py0u .icon-shape p,#mermaid-svg-mWtDMvC50Yo7Py0u .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mWtDMvC50Yo7Py0u .icon-shape .label rect,#mermaid-svg-mWtDMvC50Yo7Py0u .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mWtDMvC50Yo7Py0u .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mWtDMvC50Yo7Py0u .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mWtDMvC50Yo7Py0u :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 加载内核映像,填 boot_params
go_to_protected_mode →
protected_mode_jump
构建早期页表,使能长模式
extract_kernel 解压出真正内核
jmp 到解压后的入口
切换页表 + 建栈/IDT
jmp initial_code
x86_64_start_reservations
arch_call_rest_init
BIOS / UEFI 固件
Boot Loader(GRUB / systemd-boot 等)
① 实模式(16 位)
arch/x86/boot/main.c
② 保护模式(32 位)
compressed/head_64.S startup_32
③ 长模式(64 位)
compressed/head_64.S startup_64 + extract_kernel
④ 内核真正入口
kernel/head_64.S startup_64
⑤ 最早的 C 代码
x86_64_start_kernel
⑥ 体系无关初始化
start_kernel
⑦ BSP 变 idle 线程
rest_init → kernel_init
一句话主线:BSP 的拉起,本质是 CPU 状态(模式、页表、栈、IDT)从"裸机残骸"一步步搭建到"能跑 C 初始化代码"的过程 。整条链路的骨架是
main()(实模式)→startup_32/startup_64(compressed 自解压,切到长模式)→ 内核head_64.S的startup_64/secondary_startup_64(切页表、建栈、装 IDT)→x86_64_start_kernel(C 代码)→start_kernel(体系无关初始化)→rest_init(BSP 退居 idle,kernel_init接管)。
一、预备概念:五个术语先定义清楚
在进入源码之前,先把这条路上反复出现的五个概念钉死,否则后面每一步都会卡壳。
1.1 BSP vs AP
| 名称 | 全称 | 含义 |
|---|---|---|
| BSP | Bootstrap Processor | 加电后第一个被激活的 CPU,由 BIOS/固件指定,负责引导整个系统。本文主角。 |
| AP | Application Processor | 其余 CPU,由 BSP 在启动后期通过 INIT-SIPI-SIPI 信号逐个唤醒(下一篇主题)。 |
1.2 实模式 / 保护模式 / 长模式
这是 x86 三种 CPU 运行模式,决定了"地址怎么算、能访问多少内存、指令多宽":
| 模式 | 位宽 | 寻址方式 | 能访问内存 | 本篇角色 |
|---|---|---|---|---|
| 实模式(Real Mode) | 16 位 | 段基址 × 16 + 偏移(20 位地址线) | 1 MB | 起点,boot loader 和 main() 在此 |
| 保护模式(Protected Mode) | 32 位 | 段描述符 + 分页(可选) | 4 GB(未分页时按段) | 过渡,startup_32 在此开 PAE |
| 长模式(Long Mode) | 64 位 | 纯分页(PGD→P4D→PUD→PMD→PTE) | 2^48(4 级)或 2^57(5 级) | 终点,内核 C 代码在此运行 |
关键转折点:实模式 → 保护模式,靠设置 CR0 的 PE 位;保护模式 → 长模式,靠设置 CR4 的 PAE 位 + EFER 的 LME 位 + CR0 的 PG 位(分页使能)。后面源码里会反复看到这三个寄存器。
1.3 恒等映射(identity mapping)
页表里"虚拟地址 == 物理地址"的映射。启动早期 CPU 刚刚打开分页,如果只有"内核在高地址(如 0xffffffff81000000)"的映射,那 CPU 一旦切换 CR3,下一条指令(还停在低物理地址)就取不到------页表切换的瞬间必须有一条"旧地址→旧地址"的恒等映射垫着 ,让 CPU 能继续取指,之后再跳转到高地址。这就是为什么 head_64.S 里反复出现"identity map"和"fixup 物理地址"。
1.4 boot_params / zeropage
boot loader 在把控制权交给内核前,要按照 boot protocol 把一堆信息(内存映射、命令行、屏幕信息、内核加载地址......)填进一个叫 struct boot_params 的结构,放在一块叫 zeropage 的内存里。内核 main() 第一件事就是把它复制出来用。
c
// arch/x86/include/uapi/asm/bootparam.h (v6.6)
struct boot_params {
struct screen_info screen_info; // 屏幕信息
struct apm_bios_info apm_bios_info; // APM BIOS 信息
...
struct setup_header hdr; // boot protocol 头(最关键)
...
};
hdr(struct setup_header)里有一堆内核和 loader 之间的"约定地址" ------比如 code32_start(32 位入口地址)、kernel_alignment(内核对齐)、cmd_line_ptr(命令行指针)。后面 go_to_protected_mode() 跳转的目标就来自 hdr.code32_start。
1.5 页表层级
长模式用 4 级(或 5 级)页表把虚拟地址翻译成物理地址,每级 512 个表项:
4 级页表(48 位地址):[PGD 9位][PUD 9位][PMD 9位][PTE 9位][页内偏移 12位]
5 级页表(57 位地址):[PGD 9位][P4D 9位][PUD 9位][PMD 9位][PTE 9位][页内偏移 12位]
注意 P4D 这一级只有 5 级页表才有;4 级页表里 P4D 折叠进 PGD(Linux 用统一的五级命名兼容两种情况)。
head_64.S 里的 early_top_pgt、init_top_pgt、level3_kernel_pgt、level2_fixmap_pgt 就是内核在编译期静态预置的一套初始页表,启动早期直接用它们,不必现建。
二、实模式:main() 收集硬件信息,最后切保护模式
2.1 进入内核前的状态
BIOS/UEFI 加电自检后,把引导权交给 boot loader(GRUB 等)。boot loader 做两件事:① 把内核映像(bzImage,压缩过的)加载到内存;② 填好 boot_params。然后跳到内核的实模式入口。此时 CPU 在实模式 ,段寄存器还能直接算地址;boot_params(zeropage)的地址按 boot protocol 约定传给后续代码------32 位入口用 %esi 存它。
2.2 main() 干了什么
main() 是实模式阶段的核心,职责是"趁还能用 BIOS 中断,把所有硬件信息捞一遍",因为一旦切到保护模式,BIOS 的 16 位中断服务就再也调不了了。
c
// arch/x86/boot/main.c (v6.6, line 136)
void main(void)
{
init_default_io_ops(); // 准备 I/O 操作(inb/outb 的封装)
copy_boot_params(); // ① 把 boot header 复制到 zeropage
console_init(); // ② 初始化早期控制台
init_heap(); // ③ 初始化堆(boot 阶段内存管理)
if (validate_cpu()) { // ④ 检查 CPU 是否满足最低要求
puts("Unable to boot - please use a kernel appropriate "
"for your CPU.\n");
die();
}
set_bios_mode(); // ⑤ 告诉 BIOS 要进什么模式
detect_memory(); // ⑥ 探测内存布局(E820 映射,喂给后面 memblock)
keyboard_init(); // ⑦ 键盘
query_ist(); // ⑧ Intel SpeedStep 信息
query_apm_bios(); // ⑨ APM 信息(条件编译)
query_edd(); // ⑩ EDD 磁盘信息(条件编译)
set_video(); // ⑪ 设置显示模式
go_to_protected_mode(); // ⑫ 最后一步:切到保护模式
}
这 12 步里,与"CPU 拉起"主线最相关的是 ④ validate_cpu (CPU 型号/特性够不够)和 ⑥ detect_memory (把 E820 内存映射记下来,它是后面 memblock 和 buddy 分配器的数据来源)。其余都是"抢在 BIOS 还活着时捞信息"。
2.3 go_to_protected_mode():真正按下"模式切换"按钮
c
// arch/x86/boot/pm.c (v6.6, line 102)
void go_to_protected_mode(void)
{
realmode_switch_hook(); // 离开实模式前的钩子,顺带关中断
if (enable_a20()) { // 打开 A20 地址线(否则地址线 20 恒为 0,1MB 就绕圈)
puts("A20 gate not responding, unable to boot...\n");
die();
}
reset_coprocessor(); // 复位协处理器(浮点单元)
mask_all_interrupts(); // 屏蔽 PIC 全部中断
setup_idt(); // 建一个空的 IDT(保护模式早期没有中断)
setup_gdt(); // 建 GDT(32 位代码/数据段描述符)
protected_mode_jump(boot_params.hdr.code32_start,
(u32)&boot_params + (ds() << 4)); // 跳到 32 位入口
}
几个关键点:
- A20 门:8086 遗留问题------地址线 20 默认被拉低,实模式地址超过 1MB 会"回绕"到 0。进保护模式前必须打开它,否则高地址访问会错乱。
protected_mode_jump()(实现在arch/x86/boot/pmjump.S)是纯汇编:它设CR0.PE = 1打开保护模式,然后一个 far jump 跳到code32_start。这个code32_start对 bzImage 来说,就指向 compressed 解压器的 32 位入口startup_32。- 第二参数
(u32)&boot_params + (ds() << 4)是boot_params的物理地址,传给 32 位代码,这样后面解压内核还能继续用它。
到这里,CPU 已经出了实模式,进入保护模式。但还只有 32 位、还没分页,离跑内核 C 代码差得远。
三、自解压:compressed 从 32 位切到 64 位,原地解压出真内核
bzImage 里装的不是内核本体,而是压缩过的内核 + 一小段解压器 。这段解压器(arch/x86/boot/compressed/)的使命是:把自己切到长模式,再把压缩内核解压到正确位置,跳过去。
3.1 startup_32:开 PAE、手搓早期页表、使能长模式
c
// arch/x86/boot/compressed/head_64.S (v6.6, line 83)
SYM_FUNC_START(startup_32)
cld
cli
/* 计算"编译地址 vs 实际加载地址"的偏移 delta */
leal (BP_scratch+4)(%esi), %esp // 借 boot_params 里的 scratch 当临时栈
call 1f
1: popl %ebp
subl $ rva(1b), %ebp // %ebp = 实际加载地址 - 编译地址(delta)
/* 加载带 64 位段描述符的新 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
leal rva(boot_stack_end)(%ebp), %esp // 换真正的栈
call verify_cpu // 确认 CPU 支持长模式
testl %eax, %eax
jnz .Lno_longmode
/* 计算解压目标地址,搬到一个 2MB 对齐的安全位置 */
...
movl %cr4, %eax
orl $X86_CR4_PAE, %eax // ① 打开 PAE(长模式前提)
movl %eax, %cr4
/* ② 手搓一套 4GB 早期页表(L4/L3/L2,在 .pgtable 段预留) */
leal rva(pgtable + 0)(%ebx), %edi // Level 4
...
leal rva(pgtable)(%ebx), %eax
movl %eax, %cr3 // ③ 装载页表
movl $MSR_EFER, %ecx
rdmsr
btsl $_EFER_LME, %eax // ④ 置 EFER.LME(Long Mode Enable)
wrmsr
/* ⑤ 置 CR0.PG 打开分页,同时用 lret 跳进 64 位段 */
leal rva(startup_64)(%ebp), %eax
pushl $__KERNEL_CS
pushl %eax
movl $CR0_STATE, %eax
movl %eax, %cr0 // 开分页 + 保护
lret // far return,CS.L=1 → 进 64 位
SYM_FUNC_END(startup_32)
这段是"CPU 状态跃迁"的教科书级实现,顺序不能乱:
- 开 PAE(CR4.PAE):长模式的硬性前提。
- 建页表 :手工拼一张"前 4GB 恒等映射"的页表(
pgtable是.pgtable段里预留的一块),L2 直接铺 2048 个 2MB 大页,覆盖 4GB。 - 装 CR3:让 CPU 知道页表在哪。
- 置 EFER.LME:申请进入长模式。
- 置 CR0.PG + lret :真正打开分页那一刻,CPU 进入长模式,
lret用带CS.L=1的__KERNEL_CS段,把代码流切到 64 位。
注意:5 步里 LME 和 PG 是分开设置的------必须先 LME 再 PG。如果反过来(先开分页再申请长模式),CPU 会处于"保护模式 + 分页"的 32 位分页态,不是长模式。
3.2 startup_64:处理 5 级页表,复制到安全位置
c
// arch/x86/boot/compressed/head_64.S (v6.6, line 286)
SYM_CODE_START(startup_64)
cld
cli
xorl %eax, %eax
movl %eax, %ds / %es / %ss / %fs / %gs // 清零段寄存器
/* 计算解压后内核的起始地址(2MB 对齐) */
leaq startup_32(%rip), %rbp
... // 对齐到 kernel_alignment,最低 LOAD_PHYSICAL_ADDR
movl BP_init_size(%rsi), %ebx
subl $ rva(_end), %ebx
addq %rbp, %rbx // %rbx = 解压目标区
leaq rva(boot_stack_end)(%rbx), %rsp // 设栈
/* 关键:4 级 vs 5 级页表的切换不能原地做,要借低内存 trampoline */
call configure_5level_paging
/* 把压缩内核整个复制到 buffer 末尾,保证原地解压安全 */
leaq (_bss-8)(%rip), %rsi
leaq rva(_bss-8)(%rbx), %rdi
movl $(_bss - startup_32), %ecx
shrl $3, %ecx
std
rep movsq // 从后往前整段复制
cld
/* 重新指向复制后的 GDT,然后跳转到复制后的 .Lrelocated */
leaq rva(.Lrelocated)(%rbx), %rax
jmp *%rax
SYM_CODE_END(startup_64)
两个要点:
configure_5level_paging:长模式里改 CR4.LA57(4 级↔5 级页表)会触发#GP,必须先退出长模式再改。所以这里借助一段复制到低内存的 32 位 trampoline,来回切一次。- 自复制:解压是"原地"的,解压数据会往后膨胀、可能覆盖正在执行的代码,所以先把压缩内核整段搬到 buffer 末尾,再从那里继续执行。
3.3 .Lrelocated:解压,跳进真内核
c
// arch/x86/boot/compressed/head_64.S (v6.6, line 448)
SYM_FUNC_START_LOCAL_NOALIGN(.Lrelocated)
xorl %eax, %eax
leaq _bss(%rip), %rdi
leaq _ebss(%rip), %rcx
subq %rdi, %rcx
shrq $3, %rcx
rep stosq // 清 BSS
call load_stage2_idt // 装第二阶段 IDT
movq %r15, %rdi
call initialize_identity_maps // 为解压目标区建恒等映射
movq %r15, %rdi // %rdi = boot_params
movq %rbp, %rsi // %rsi = 解压目标地址
call extract_kernel // 解压,返回内核入口点存 %rax
movq %r15, %rsi
jmp *%rax // 跳到解压后的真正内核入口
SYM_FUNC_END(.Lrelocated)
extract_kernel()(实现在 misc.c)负责挑一个解压算法(gzip/LZ4/XZ/zstd 等,编译期选定)解压内核,然后返回解压后内核的入口地址 。jmp *%rax 跳过去------这个地址,就是内核 arch/x86/kernel/head_64.S 里的 startup_64。
到这里,压缩内核已经变成真正的
vmlinux,CPU 也已经在长模式里了。接下来进入内核自己的、也是真正的入口。
四、内核真正入口:head_64.S 切页表、建栈、装 IDT
这是内核映像(非 compressed)的第一段代码。注意它也有一个 startup_64,但和上一节 compressed 的同名符号不是同一个 ------这是内核本体在 arch/x86/kernel/head_64.S 里的入口。
4.1 startup_64:给 BSP 做最后的环境搭建
c
// arch/x86/kernel/head_64.S (v6.6, line 46)
SYM_CODE_START_NOALIGN(startup_64)
UNWIND_HINT_END_OF_STACK
/*
* 此时 CPU 已在 64 位(CS.L=1, CS.D=0),有人帮我们加载了恒等映射页表。
* %RSI = boot_params 的物理地址,存进 %R15(callee-saved,防 C 调用覆盖)。
*/
mov %rsi, %r15
leaq (__end_init_task - PTREGS_SIZE)(%rip), %rsp // ① 设栈(init 任务的栈底)
leaq _text(%rip), %rdi
/* ② 设 GSBASE 指向 fixed_percpu_data,让 C 代码能读栈金丝雀 */
movl $MSR_GS_BASE, %ecx
leaq INIT_PER_CPU_VAR(fixed_percpu_data)(%rip), %rdx
movl %edx, %eax
shrq $32, %rdx
wrmsr
call startup_64_setup_env // ③ 加载 GDT + 重载段寄存器 + 装 bringup IDT
/* ④ 切换到 __KERNEL_CS,让 IRET 之后 CS 正确 */
pushq $__KERNEL_CS
leaq .Lon_kernel_cs(%rip), %rax
pushq %rax
lretq
.Lon_kernel_cs:
call sme_enable // ⑤ AMD SME/SEV 内存加密(条件编译)
call verify_cpu // ⑥ 校验 CPU 配置(NX 等)
leaq _text(%rip), %rdi
movq %r15, %rsi
call __startup_64 // ⑦ 页表 fixup,返回 early_top_pgt 物理地址
addq $(early_top_pgt - __START_KERNEL_map), %rax // 形成 CR3 值
jmp 1f // ⑧ 落到下面的 secondary_startup_64 继续
SYM_CODE_END(startup_64)
__startup_64()(实现在 head64.c,unsigned long __head __startup_64(unsigned long physaddr, struct boot_params *bp))是这里的核心:它计算实际加载地址与编译地址的 delta ,然后修正静态页表里所有"硬编码的物理地址" ,使 early_top_pgt 这套页表指向真正的加载位置。返回的 %rax 就是修正后的页表基址,addq $(early_top_pgt - __START_KERNEL_map) 把它换算成 CR3 要的值。
关键:内核是可重定位 的(CONFIG_RELOCATABLE),编译期假定自己跑在
__START_KERNEL_map附近的高地址,但实际加载地址由 boot loader 决定。__startup_64干的就是"按实际地址修正页表",否则一切换 CR3 就取不到指。
4.2 secondary_startup_64:BSP 和 AP 共用的后半段
startup_64 最后 jmp 1f 直接落到这里。这个入口BSP 和 AP 共用 ------BSP 从 startup_64 落下来(走物理地址),AP 从 trampoline 进来(走虚拟地址)。这也是下一篇讲 AP 时还要回来的地方。
c
// arch/x86/kernel/head_64.S (v6.6, line 121)
SYM_CODE_START(secondary_startup_64)
call verify_cpu // 再校验一次
/* 形成 CR3(含 SME 加密掩码),BSP 用 early_top_pgt,AP 用 init_top_pgt */
addq $(init_top_pgt - __START_KERNEL_map), %rax
1:
/* 保留 CR4.MCE,打开 PAE/PGE,按需打开 LA57 */
movq %cr4, %rcx
andl $X86_CR4_MCE, %ecx
orl $(X86_CR4_PAE | X86_CR4_PGE), %ecx
...
movq %rcx, %cr4
addq phys_base(%rip), %rax // 加 phys_base 修正
movq %rax, %rdi
call sev_verify_cbit // SEV 校验 C-bit
movq %rax, %cr3 // ① 切换页表!
/* ② 全局 TLB flush(切换 CR3 后,冲掉旧恒等映射的 TLB) */
movq %cr4, %rcx
movq %rcx, %rax
xorq $X86_CR4_PGE, %rcx
movq %rcx, %cr4
movq %rax, %cr4
/* ③ 确保自己在虚拟地址上执行(下一步 jmp 到标号 1 的虚拟地址) */
movq $1f, %rax
jmp *%rax
1:
/* ④ 找出自己是几号 CPU(BSP 就是 0),得到 per-cpu offset */
movl smpboot_control(%rip), %ecx
...
.Lsetup_cpu:
movq __per_cpu_offset(,%rcx,8), %rdx // %rdx = per-cpu offset
/* ⑤ 换到本 CPU 自己的栈 */
movq pcpu_hot + X86_current_task(%rdx), %rax
movq TASK_threadsp(%rax), %rsp
/* ⑥ 释放 trampoline 锁(BSP 的指针是 NULL,跳过) */
movq trampoline_lock(%rip), %rax
testq %rax, %rax
jz .Lsetup_gdt
movl $0, (%rax)
.Lsetup_gdt:
/* ⑦ 加载内核态 GDT + 清数据段 + 设 %gs */
subq $16, %rsp
movw $(GDT_SIZE-1), (%rsp)
leaq gdt_page(%rdx), %rax
movq %rax, 2(%rsp)
lgdt (%rsp)
addq $16, %rsp
xorl %eax, %eax
movl %eax, %ds / %ss / %es / %fs / %gs
movl $MSR_GS_BASE, %ecx
...
wrmsr
call early_setup_idt // ⑧ 装 bringup IDT
/* ⑨ 检查 NX,设置 EFER(开 SCE + NX)和 CR0 */
movl $0x80000001, %eax
cpuid
movl %edx, %edi
movl $MSR_EFER, %ecx
rdmsr
btsl $_EFER_SCE, %eax
btl $20, %edi
jnc 1f
btsl $_EFER_NX, %eax
btsq $_PAGE_BIT_NX, early_pmd_flags(%rip)
1: wrmsr
movl $CR0_STATE, %eax
movq %rax, %cr0
pushq $0
popfq // 清 EFLAGS
movq %r15, %rdi // ⑩ %rdi = boot_params(BSP 才有,AP 已清零)
.Ljump_to_C_code:
pushq $.Lafter_lret
xorl %ebp, %ebp
movq initial_code(%rip), %rax // ⑪ initial_code = x86_64_start_kernel
pushq $__KERNEL_CS
pushq %rax
lretq // far return 到 C 代码
SYM_CODE_END(secondary_startup_64)
这一段是整个拉起过程里信息量最大的汇编,它完成了从"裸机状态"到"能进 C 函数"的全部收尾。按顺序拆成 5 组:
- 切页表(①②③) :
movq %rax, %cr3是分水岭------之前完全靠恒等映射(虚拟==物理)取指,之后切到early_top_pgt(高地址映射,恒等映射暂时保留,直到x86_64_start_kernel的reset_early_page_tables才拆)。切完必须全局 TLB flush,并jmp *%rax到虚拟地址标号,确保下一条指令从新页表取。 - 找 CPU 号 + per-cpu(④⑤) :BSP 走
smpboot_control的默认值(CPU 0),拿到__per_cpu_offset[0],再通过pcpu_hot.current_task找到init_task的栈,换上。此时 BSP 终于有了自己的内核栈。 - GDT/段/IDT(⑥⑦⑧) :装内核态 GDT,清掉实模式遗留的段选择子,设
%gs(指向 fixed_percpu_data),装 bringup IDT。 - EFER/CR0(⑨):开 System Call(SCE)、开 NX(禁止执行),设 CR0 的最终状态。
- 进 C(⑩⑪) :
%rdi传boot_params,lretqfar return 到initial_code。
initial_code 的值在文件末尾定义:
c
// arch/x86/kernel/head_64.S (v6.6, line 495)
SYM_DATA(initial_code, .quad x86_64_start_kernel)
于是,CPU 经过层层搭建,终于带着"页表、栈、GDT、IDT、GSBASE"齐全的状态,跳进了第一个 C 函数
x86_64_start_kernel。
五、C 代码接管:x86_64_start_kernel → start_kernel
5.1 x86_64_start_kernel:x86 专属的收尾
c
// arch/x86/kernel/head64.c (v6.6, line 474)
asmlinkage __visible void __init __noreturn x86_64_start_kernel(char *real_mode_data)
{
BUILD_BUG_ON(...); // 一堆编译期地址布局断言
cr4_init_shadow(); // 初始化 CR4 的影子值
reset_early_page_tables(); // 杀掉恒等映射 trampoline(不再需要)
clear_bss(); // 清 BSS 段
clear_page(init_top_pgt); // 清 init_top_pgt(保留高映射前先清)
sme_early_init(); // SME 内存加密早期初始化
kasan_early_init(); // KASAN 早期初始化
__native_tlb_flush_global(this_cpu_read(cpu_tlbstate.cr4)); // 冲 TLB
idt_setup_early_handler(); // 建 early IDT 处理器
tdx_early_init(); // Intel TDX 早期初始化
copy_bootdata(__va(real_mode_data)); // 复制 boot_params 到内核空间
load_ucode_bsp(); // 在 BSP 上加载微码
init_top_pgt[511] = early_top_pgt[511]; // 建内核高地址映射
x86_64_start_reservations(real_mode_data); // 进入下一层
}
这里的主题是"从物理地址世界过渡到虚拟地址世界,并把 boot loader 给的东西搬进内核 ":copy_bootdata() 把实模式的 boot_params 复制到内核空间;load_ucode_bsp() 加载微码;reset_early_page_tables() 拆掉恒等映射(此时内核已经能稳定跑在虚拟地址上了)。
5.2 x86_64_start_reservations:一层薄薄的中转
c
// arch/x86/kernel/head64.c (v6.6, line 540)
void __init __noreturn x86_64_start_reservations(char *real_mode_data)
{
if (!boot_params.hdr.version)
copy_bootdata(__va(real_mode_data)); // 兜底复制
x86_early_init_platform_quirks(); // 早期平台 quirk
switch (boot_params.hdr.hardware_subarch) {
case X86_SUBARCH_INTEL_MID:
x86_intel_mid_early_setup();
break;
default:
break;
}
start_kernel(); // ① 进入体系无关的 start_kernel
}
做完 x86 专属的早期准备后,调用 start_kernel()。从这一刻起,代码进入 init/main.c,开始和具体 CPU 架构无关的通用初始化。
5.3 start_kernel:把整个世界准备好
start_kernel()(init/main.c:870)是内核启动的"主调度"函数,调用了几十个初始化函数。逐行贴没意义,这里按主题分组,突出与"CPU 拉起"相关的几个关键点(完整序列见附录):
c
// init/main.c (v6.6, line 870)
asmlinkage __visible __init ... void start_kernel(void)
{
char *command_line;
char *after_dashes;
set_task_stack_end_magic(&init_task); // ① init_task 栈底写魔数(溢出检测)
smp_setup_processor_id(); // ② 架构钩子(x86 上为空实现)
debug_objects_early_init();
init_vmlinux_build_id();
cgroup_init_early();
local_irq_disable(); // ③ 关中断(全程开着就乱套)
early_boot_irqs_disabled = true;
boot_cpu_init(); // ④ 把 BSP 标记为 online/present/possible
page_address_init();
pr_notice("%s", linux_banner); // ⑤ 打印 "Linux version ..."
early_security_init();
setup_arch(&command_line); // ⑥ x86 架构初始化(大头)
setup_boot_config();
setup_command_line(command_line);
setup_nr_cpu_ids(); // ⑦ 确定 CPU 总数上限
setup_per_cpu_areas(); // ⑧ 建 per-cpu 内存区
smp_prepare_boot_cpu(); // ⑨ BSP 专属钩子(见下)
boot_cpu_hotplug_init(); // ⑩ 初始化 hotplug 状态机(BSP 置 ONLINE)
...
sched_init(); // ⑪ 初始化调度器
...
init_IRQ(); // ⑫ 初始化中断
tick_init(); // ⑬ 时钟事件
...
time_init(); // ⑭ 时钟源
...
local_irq_enable(); // ⑮ 到这里才开中断
...
console_init(); // ⑯ 初始化控制台
...
arch_call_rest_init(); // ⑰ 进入 rest_init(收尾)
}
几个与"CPU"强相关的点单独展开:
④ boot_cpu_init()------BSP 第一次被"登记"成一台合法 CPU:
c
// kernel/cpu.c (v6.6, line 3155)
void __init boot_cpu_init(void)
{
int cpu = smp_processor_id(); // 返回 0(BSP)
set_cpu_online(cpu, true); // online / active / present / possible 四态全置位
set_cpu_active(cpu, true);
set_cpu_present(cpu, true);
set_cpu_possible(cpu, true);
#ifdef CONFIG_SMP
__boot_cpu_id = cpu;
#endif
}
这四张 cpumask(possible/present/active/online)是内核管理 CPU 集合的基础,后面 hotplug 状态机(下一篇会讲)就是在这些 mask 上做增删。
⑨ smp_prepare_boot_cpu() ------它是 init/main.c:898 调用的架构钩子,最终落到 x86 的:
c
// arch/x86/kernel/smpboot.c (v6.6, line 1246)
void __init native_smp_prepare_boot_cpu(void)
{
int me = smp_processor_id();
if (!IS_ENABLED(CONFIG_SMP)) // 单核配置下,这里直接换 GDT + per-cpu 基址
switch_gdt_and_percpu_base(me);
native_pv_lock_init(); // paravirt 锁初始化
}
在 SMP 配置下它几乎什么都不做(真正的 GDT/per-cpu 切换早就在 setup_per_cpu_areas() 里做了)------到这一步,BSP 的"启动准备"就全部结束了。
⑩ boot_cpu_hotplug_init()------把 BSP 登记进 hotplug 状态机:
c
// kernel/cpu.c (v6.6, line 3173)
void __init boot_cpu_hotplug_init(void)
{
#ifdef CONFIG_SMP
cpumask_set_cpu(smp_processor_id(), &cpus_booted_once_mask);
atomic_set(this_cpu_ptr(&cpuhp_state.ap_sync_state), SYNC_STATE_ONLINE);
#endif
this_cpu_write(cpuhp_state.state, CPUHP_ONLINE); // BSP 直接置为 ONLINE
this_cpu_write(cpuhp_state.target, CPUHP_ONLINE);
}
这里第一次出现了 cpuhp_state(hotplug 状态机),BSP 不走 cpu_up() 的完整状态流转,而是直接被写成 CPUHP_ONLINE------因为它本来就是"已经上线"的。
setup_arch()(arch/x86/kernel/setup.c:854)是这几十个调用里最重 的一个:它在 x86 侧做early_cpu_init(探测 CPU 特性)、e820__memory_setup(解析 E820 内存映射,setup.c:944)、early_reserve_memory(预留内核/固件占用)、e820__memblock_setup(把 E820 转成memblock,setup.c:1117)、x86_configure_nx(NX 配置)等。不过内存初始化的细节属于另一个系列(物理内存初始化、buddy 分配器),这里点到为止。
六、收尾:rest_init 让 BSP 退居 idle,kernel_init 接管
start_kernel() 最后一件事是调 arch_call_rest_init()(init/main.c:1068),它是个弱符号,默认直接调 rest_init():
c
// init/main.c (v6.6, line 680)
noinline void __ref __noreturn rest_init(void)
{
struct task_struct *tsk;
int pid;
rcu_scheduler_starting();
/* ① 先 spawn init 进程(pid 1 = kernel_init) */
pid = user_mode_thread(kernel_init, NULL, CLONE_FS);
/* ② 把 init 钉在 BSP 上(此时 task 迁移还没就绪) */
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();
numa_default_policy();
/* ③ 再 spawn kthreadd(pid 2,所有内核线程的祖先) */
pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES);
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
system_state = SYSTEM_SCHEDULING;
complete(&kthreadd_done);
/* ④ BSP 自己变成 idle 线程,进入 idle 循环,从此不再"主导"启动 */
schedule_preempt_disabled();
cpu_startup_entry(CPUHP_ONLINE);
}
这是 BSP 拉起的最后一幕,也回答了"BSP 启动完去哪了":
- pid 1 =
kernel_init:未来要跑smp_init()拉起 AP、挂载根文件系统、执行用户态/sbin/init。 - pid 2 =
kthreadd:所有内核线程(kworker 等)的祖先。 - BSP 自己 :调用
cpu_startup_entry(CPUHP_ONLINE),进入 idle 循环 (do_idle),在没活干时反复schedule()让出 CPU。
注意
user_mode_thread(kernel_init, ...)先于kernel_thread(kthreadd, ...)被创建,注释里写得明白:必须让 init 拿到 pid 1,但 init 后面要创建 kthread,而 kthreadd 还没起来,所以让 init 先创建、再等 kthreadd 的完成量(wait_for_completion(&kthreadd_done))。
kernel_init 后续的 kernel_init_freeable()(init/main.c:1518)里有 smp_prepare_cpus() 和 smp_init()------那正是唤醒 AP 的地方,属于下一篇的主题。整个 BSP 拉起的故事,到这里画上句号:
c
// init/main.c (v6.6, line 1540)
smp_init(); // 唤醒所有 AP(下一篇讲)
sched_init_smp(); // 完整拓扑建立
七、总结:记住三点
-
CPU 状态跃迁是主线 :实模式(16 位,1MB)→ 保护模式(32 位)→ 长模式(64 位),靠几个关键寄存器动作完成------
CR0.PE(进保护模式)、CR4.PAE + EFER.LME + CR0.PG(进长模式)、mov %rax, %cr3(换内核页表)。读懂这条线,整篇源码就串起来了。 -
页表是贯穿全程的"接力棒" :从
startup_32手搓 4GB 恒等映射,到__startup_64按实际加载地址 fixup 静态页表,到secondary_startup_64切到内核高地址映射------每一步都是"先有能用的页表,才能切过去"。恒等映射是"切换瞬间的垫脚石"。 -
BSP 的终点是 idle :BSP 不负责"永远主导系统",它把 init(pid 1)和 kthreadd(pid 2)创建出来后,自己
cpu_startup_entry()进 idle 循环。之后的 AP 唤醒(smp_init)、hotplug 状态机(cpuhp_state)才是"多核世界"的开始------这两个正是 CPU 系列接下来的两篇。
附录:start_kernel() 初始化序列速查
按 init/main.c:870 的实际调用顺序(已按主题归组,省略号表示中间有别的调用):
| 阶段 | 关键调用 | 行号 | 职责 |
|---|---|---|---|
| 栈/CPU | set_task_stack_end_magic / smp_setup_processor_id |
875-876 | init 栈魔数、架构钩子(x86 为空实现) |
| 关中断 | local_irq_disable |
882 | 全程先关中断 |
| CPU 登记 | boot_cpu_init |
889 | BSP 四态置位 |
| 架构初始化 | setup_arch |
893 | x86 早期 + E820 + memblock |
| per-cpu | setup_per_cpu_areas |
897 | 建 per-cpu 内存 |
| BSP 钩子 | smp_prepare_boot_cpu / boot_cpu_hotplug_init |
898-899 | BSP 启动收尾 + 置 ONLINE |
| 内存 | setup_log_buf / mm_core_init |
924/928 | 日志缓冲、mm 核心 |
| 调度器 | sched_init |
940 | 调度器初始化 |
| 中断 | early_irq_init / init_IRQ |
971-972 | 中断初始化 |
| 时钟 | tick_init / time_init |
973/980 | 时钟事件/时钟源 |
| 开中断 | local_irq_enable |
995 | 到这里才开中断 |
| 控制台 | console_init |
1004 | 控制台可用 |
| 收尾 | arch_call_rest_init |
1068 | 进 rest_init |
版本边界:本文只覆盖 x86、64 位 (
CONFIG_X86_64)的 BSP 拉起路径。32 位(head_32.S→i386_start_kernel)和 ARM64/RISC-V 的对应路径结构不同,不在本文范围。