Linux 6.6内核 CPU 深度解析(一):BSP 拉起 — 从实模式到 kernel_init 的完整旅程

〇、全景:一个 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 状态跃迁"的教科书级实现,顺序不能乱:

  1. 开 PAE(CR4.PAE):长模式的硬性前提。
  2. 建页表 :手工拼一张"前 4GB 恒等映射"的页表(pgtable 是 .pgtable 段里预留的一块),L2 直接铺 2048 个 2MB 大页,覆盖 4GB。
  3. 装 CR3:让 CPU 知道页表在哪。
  4. 置 EFER.LME:申请进入长模式。
  5. 置 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 组:

  1. 切页表(①②③) :movq %rax, %cr3 是分水岭------之前完全靠恒等映射(虚拟==物理)取指,之后切到 early_top_pgt(高地址映射,恒等映射暂时保留,直到 x86_64_start_kernel 的 reset_early_page_tables 才拆)。切完必须全局 TLB flush,并 jmp *%rax 到虚拟地址标号,确保下一条指令从新页表取。
  2. 找 CPU 号 + per-cpu(④⑤) :BSP 走 smpboot_control 的默认值(CPU 0),拿到 __per_cpu_offset[0],再通过 pcpu_hot.current_task 找到 init_task 的栈,换上。此时 BSP 终于有了自己的内核栈。
  3. GDT/段/IDT(⑥⑦⑧) :装内核态 GDT,清掉实模式遗留的段选择子,设 %gs(指向 fixed_percpu_data),装 bringup IDT。
  4. EFER/CR0(⑨):开 System Call(SCE)、开 NX(禁止执行),设 CR0 的最终状态。
  5. 进 C(⑩⑪) :%rdi 传 boot_params,lretq far 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 启动完去哪了":

  1. pid 1 = kernel_init :未来要跑 smp_init() 拉起 AP、挂载根文件系统、执行用户态 /sbin/init。
  2. pid 2 = kthreadd:所有内核线程(kworker 等)的祖先。
  3. 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();  // 完整拓扑建立

七、总结:记住三点

  1. CPU 状态跃迁是主线 :实模式(16 位,1MB)→ 保护模式(32 位)→ 长模式(64 位),靠几个关键寄存器动作完成------CR0.PE(进保护模式)、CR4.PAE + EFER.LME + CR0.PG(进长模式)、mov %rax, %cr3(换内核页表)。读懂这条线,整篇源码就串起来了。

  2. 页表是贯穿全程的"接力棒" :从 startup_32 手搓 4GB 恒等映射,到 __startup_64 按实际加载地址 fixup 静态页表,到 secondary_startup_64 切到内核高地址映射------每一步都是"先有能用的页表,才能切过去"。恒等映射是"切换瞬间的垫脚石"。

  3. 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 的对应路径结构不同,不在本文范围。

相关推荐
赵民勇1 小时前
dconf命令详解
linux·运维
代码方舟1 小时前
零信任架构实战:基于天远学历信息高级版构建自动化智库入驻审查网关
运维·人工智能·架构·自动化
ggaofeng1 小时前
Ctrl+C结束进程是如何实现的
服务器·c语言·网络
和裕2 小时前
年度框架直供 vs 零散按需采购:定制纸箱采购成本、交付与服务核心区别全对比
大数据·运维·网络·人工智能·算法
qeen872 小时前
【Linux】Linux环境下的进程终止与进程等待问题
linux·运维·服务器
机核研创社4 小时前
短裤自动化工序与设备清单:十个点位、三项技术要点与选型裁决顺序
运维·自动化
尹人入圣4 小时前
市面上IP驱动产业新场景新工具
运维·网络·python·tcp/ip
衍玊4 小时前
常见i/o模型以及高性能服务器设计
服务器
Dovis(誓平步青云)4 小时前
多个链接不等于多份证据,新闻核验看板怎样合并来源
java·服务器·前端·javascript·人工智能·pdf·电脑