Linux启动流程初探(V:6.12.32)
版本说明: Linux 6.12.32
架构说明: x86_64
这个文章其实是因为看 arttnba3 大佬的文章的时候看见了一个泄漏地址小trick也就是 physmem_base + 0x9d000 存放着secondary_startup_64指针,从而泄漏 kernel base.但是搜索了很多文章好像都只是提及了一下这个小trick但是没说明原因。然后在询问了 Tplus 大佬后,知道了这个板块是realmode相关的,所以从头开始进行了分析。最终结合自己的理解推测了一下这个trick的原因,当然受限于本人的水平,可能这个推断并不正确。(如果有问题请各位大佬帮忙指正一下)
想要快速看这个问题的答案可以直接跳转到最后的总结,因为本文很多过程都是针对于Linux启动过程中的内存变化,可能略显啰嗦(可能有点点偏离主题)。
参考文章:

总览
流程图
┌───────────────────────────────────────────────────────────────────────┐
│ [阶段 0] BIOS/UEFI 阶段 │
├───────────────────────────────────────────────────────────────────────┤
│ CPU 状态: 16位实模式 │
│ 物理内存: 0xFFFF0000 (BIOS ROM) │
│ │
│ 执行流程: │
│ 1. 加电自检 (POST) │
│ 2. 初始化硬件 │
│ 3. 从引导设备读取第一扇区 (512字节) → 0x7C00 │
│ 4. 跳转到 0x7C00 执行 Bootloader │
└───────────────────────────────┬───────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────────────┐
│ [阶段 1] Bootloader 阶段 (GRUB/LILO) │
├───────────────────────────────────────────────────────────────────────┤
│ 执行位置: 0x7C00 起始 │
│ CPU 状态: 16位实模式 │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Bootloader 的工作: │ │
│ │ │ │
│ │ 1. 读取 bzImage 文件 (内核镜像) │ │
│ │ bzImage 结构: │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ [0x0000-0x01FF] Legacy Boot Sector │ (已废弃) │ │
│ │ ├──────────────────────────────────────────┤ │ │
│ │ │ [0x0200-0x???] Setup.bin │ ← 第一部分 │ │
│ │ │ (header.S + main.c...) │ │ │
│ │ │ 实模式代码,约 32KB │ │ │
│ │ ├──────────────────────────────────────────┤ │ │
│ │ │ [Setup 后] vmlinux.bin.gz │ ← 第二部分 │ │
│ │ │ (head_64.S + 压缩内核) │ │ │
│ │ │ 压缩的保护/长模式代码 │ │ │
│ │ │ 几 MB 大小 │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 2. 内存加载分配: │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ 0x00090000 ← Setup.bin 加载到这里 │ │ │
│ │ │ (arch/x86/boot/header.S) │ │ │
│ │ │ 包含 hdr 结构体定义 │ │ │
│ │ ├────────────────────────────────────────────┤ │ │
│ │ │ 0x00100000 ← vmlinux.bin.gz 加载到这里 │ │ │
│ │ │ (arch/x86/boot/compressed/ │ │ │
│ │ │ head_64.S::startup_32) │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 3. 设置 boot_params (zeropage): │ │
│ │ - 分配 4KB 内存 (通常在 0x10000 附近) │ │
│ │ - 填充内核命令行、内存映射等信息 │ │
│ │ - %esi 寄存器指向 boot_params │ │
│ │ │ │
│ │ 4. 跳转到 0x90000 开始执行内核 │ │
│ │ jmp 0x9000:0x0200 (实模式段:偏移) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────┬───────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────────────┐
│ [阶段 2] Setup 代码执行 - arch/x86/boot/ │
│ (16位实模式,在 0x90000 运行) │
├───────────────────────────────────────────────────────────────────────┤
│ 执行位置: 0x90000 (物理地址) │
│ CPU 状态: 16位实模式 │
│ 关键文件: arch/x86/boot/header.S, main.c, pm.c │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 阶段 2.1: header.S::_start (0x90000 + 0x200) │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 1. 段寄存器初始化 │ │ │
│ │ │ movw %ds, %ax ; DS = 0x9000 (Bootloader设置的) │ │ │
│ │ │ movw %ax, %es ; ES = DS │ │ │
│ │ │ cld ; 清除方向标志 │ │ │
│ │ │ │ │ │
│ │ │ 2. 设置栈 (检查 SS 是否有效) │ │ │
│ │ │ 若无效: SS = DS, SP = _end + STACK_SIZE │ │ │
│ │ │ │ │ │
│ │ │ 3. CS 段规范化 │ │ │
│ │ │ pushw %ds ; 压入 DS │ │ │
│ │ │ pushw $6f ; 压入返回地址 │ │ │
│ │ │ lretw ; 远返回,使 CS = DS │ │ │
│ │ │ │ │ │
│ │ │ 4. 验证 setup 签名 │ │ │
│ │ │ cmpl $0x5a5aaa55, setup_sig │ │ │
│ │ │ │ │ │
│ │ │ 5. 清零 BSS 段 │ │ │
│ │ │ movw $__bss_start, %di │ │ │
│ │ │ rep stosl │ │ │
│ │ │ │ │ │
│ │ │ 6. 调用 C 代码 │ │ │
│ │ │ calll main ; 跳转到 main.c::main() │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ↓ │ │
│ │ │ │
│ │ 阶段 2.2: main.c::main() (0x90000 + offset) │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 1. copy_boot_params() ★关键步骤★ │ │ │
│ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ 目的: 将 header.S 中的 hdr 结构复制到 boot_params.hdr│ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ 源地址: &hdr (在 header.S 第 220 行定义) │ │ │ │
│ │ │ │ 物理地址 ≈ 0x90000 + 0x1F1 │ │ │ │
│ │ │ │ 包含: setup_sects, code32_start, 等字段 │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ 目标地址: &boot_params.hdr │ │ │ │
│ │ │ │ boot_params 是 BSS 段的全局变量 │ │ │ │
│ │ │ │ 大小: 4096 字节 │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ 操作: │ │ │ │
│ │ │ │ memcpy(&boot_params.hdr, &hdr, sizeof(hdr)); │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ 结果: boot_params 现在包含完整的启动参数 │ │ │ │
│ │ │ │ 包括 code32_start = 0x100000 │ │ │ │
│ │ │ └───────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ 2. console_init() - 初始化早期控制台输出 │ │ │
│ │ │ 3. init_heap() - 初始化堆 (用于内存分配) │ │ │
│ │ │ 4. validate_cpu() - 检查 CPU 是否支持 long mode │ │ │
│ │ │ 5. set_bios_mode() - 告知 BIOS 将进入 64位模式 │ │ │
│ │ │ 6. detect_memory() - 通过 INT 15h 获取 E820 内存映射 │ │ │
│ │ │ 结果保存到 boot_params.e820_table │ │ │
│ │ │ 7. keyboard_init() - 初始化键盘控制器 │ │ │
│ │ │ 8. set_video() - 设置视频模式 │ │ │
│ │ │ 9. go_to_protected_mode() - 跳转到保护模式 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ↓ │ │
│ │ │ │
│ │ 阶段 2.3: pm.c::go_to_protected_mode() │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 1. realmode_switch_hook() - 调用 BIOS 钩子 │ │ │
│ │ │ 2. enable_a20() - 启用 A20 地址线 │ │ │
│ │ │ 允许访问 > 1MB 内存 │ │ │
│ │ │ 3. reset_coprocessor() - 重置协处理器 │ │ │
│ │ │ 4. mask_all_interrupts() - 屏蔽所有中断 │ │ │
│ │ │ 5. setup_idt() - 设置空 IDT (中断向量表) │ │ │
│ │ │ 6. setup_gdt() - 设置临时 GDT (全局描述符表) │ │ │
│ │ │ GDT 包含: │ │ │
│ │ │ - NULL 描述符 │ │ │
│ │ │ - __BOOT_CS (代码段) │ │ │
│ │ │ - __BOOT_DS (数据段) │ │ │
│ │ │ 7. protected_mode_jump() - 切换到保护模式 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ↓ │ │
│ │ │ │
│ │ 阶段 2.4: pmjump.S::protected_mode_jump() │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 参数: │ │ │
│ │ │ %eax = boot_params.hdr.code32_start (= 0x100000) │ │ │
│ │ │ %esi = &boot_params (物理地址) │ │ │
│ │ │ │ │ │
│ │ │ 1. 保存参数 │ │ │
│ │ │ movl %edx, %esi ; %esi = boot_params 地址 │ │ │
│ │ │ │ │ │
│ │ │ 2. 启用保护模式 │ │ │
│ │ │ movl %cr0, %edx │ │ │
│ │ │ orb $X86_CR0_PE, %dl ; 设置 CR0.PE = 1 │ │ │
│ │ │ movl %edx, %cr0 │ │ │
│ │ │ │ │ │
│ │ │ 3. 远跳转到 32位代码 (刷新流水线) │ │ │
│ │ │ .byte 0x66, 0xea ; ljmpl 操作码 │ │ │
│ │ │ .long .Lin_pm32 ; 偏移 │ │ │
│ │ │ .word __BOOT_CS ; 段选择子 │ │ │
│ │ │ │ │ │
│ │ │ 4. .Lin_pm32: (32位保护模式) │ │ │
│ │ │ movl %ecx, %ds ; 设置所有数据段寄存器 │ │ │
│ │ │ movl %ecx, %es │ │ │
│ │ │ movl %ecx, %ss │ │ │
│ │ │ xorl %ecx, %ecx ; 清空寄存器 │ │ │
│ │ │ xorl %edx, %edx │ │ │
│ │ │ xorl %ebx, %ebx │ │ │
│ │ │ │ │ │
│ │ │ 5. 跳转到压缩内核入口 │ │ │
│ │ │ jmpl *%eax ; 跳转到 0x100000 │ │ │
│ │ │ ; ★离开 0x90000 区域★ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ★ Setup 代码在 0x90000 的使命完成!★ │
│ ★ 这块内存现在空闲,将来会被 realmode trampoline 重用 ★ │
└───────────────────────────────┬───────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────────────┐
│ [阶段 3] 压缩内核执行 - arch/x86/boot/compressed/head_64.S │
│ startup_32 (32位保护模式,在 0x100000 运行) │
├───────────────────────────────────────────────────────────────────────┤
│ 执行位置: 0x100000 (物理地址) │
│ CPU 状态: 32位保护模式 │
│ 关键文件: arch/x86/boot/compressed/head_64.S │
│ 寄存器: %esi = boot_params 物理地址 │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ PHASE 1: 地址重定位计算 (行 90-105) │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 90: cld # 清除方向标志 │ │ │
│ │ │ 91: cli # 禁用中断 │ │ │
│ │ │ │ │ │
│ │ │ 101: leal (BP_scratch+4)(%esi), %esp │ │ │
│ │ │ # 设置临时栈: boot_params.scratch + 4 │ │ │
│ │ │ # BP_scratch = boot_params 结构中的偏移量 │ │ │
│ │ │ │ │ │
│ │ │ 102: call 1f # 调用下一条指令 │ │ │
│ │ │ 103: 1: popl %ebp # %ebp = 当前 EIP (返回地址) │ │ │
│ │ │ 104: subl $ rva(1b), %ebp # %ebp -= 编译时地址 │ │ │
│ │ │ # rva(1b) = (1b - startup_32) │ │ │
│ │ │ │ │ │
│ │ │ 原理: "call/pop" 技巧获取当前运行地址 │ │ │
│ │ │ - call 压入返回地址 (当前 EIP) │ │ │
│ │ │ - pop 获取这个地址到 %ebp │ │ │
│ │ │ - 减去编译时的偏移,得到加载基址 │ │ │
│ │ │ │ │ │
│ │ │ 结果: %ebp = 实际物理加载地址 (通常是 0x100000) │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ PHASE 2: 设置 GDT 和段寄存器 (行 106-126) │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 107: leal rva(gdt)(%ebp), %eax │ │ │
│ │ │ # %eax = gdt 的实际物理地址 │ │ │
│ │ │ # rva(gdt) = gdt - startup_32 (相对偏移) │ │ │
│ │ │ │ │ │
│ │ │ 108: movl %eax, 2(%eax) # 修正 GDT 描述符中的基址字段 │ │ │
│ │ │ 109: lgdt (%eax) # 加载 GDT │ │ │
│ │ │ │ │ │
│ │ │ 112-117: 设置所有段寄存器为 __BOOT_DS │ │ │
│ │ │ movl $__BOOT_DS, %eax │ │ │
│ │ │ movl %eax, %ds │ │ │
│ │ │ movl %eax, %es │ │ │
│ │ │ movl %eax, %fs │ │ │
│ │ │ movl %eax, %gs │ │ │
│ │ │ movl %eax, %ss │ │ │
│ │ │ │ │ │
│ │ │ 120: leal rva(boot_stack_end)(%ebp), %esp │ │ │
│ │ │ # 设置栈指针到 boot_stack_end │ │ │
│ │ │ │ │ │
│ │ │ 122-125: 使用 lretl 更新 CS │ │ │
│ │ │ pushl $__KERNEL32_CS │ │ │
│ │ │ leal rva(1f)(%ebp), %eax │ │ │
│ │ │ pushl %eax │ │ │
│ │ │ lretl # 远返回,CS = __KERNEL32_CS │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ PHASE 3: CPU 验证和地址计算 (行 133-162) │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 134: call verify_cpu # 验证 CPU 是否支持 64位 │ │ │
│ │ │ testl %eax, %eax │ │ │
│ │ │ jnz .Lno_longmode # 不支持则报错 │ │ │
│ │ │ │ │ │
│ │ │ 148: movl %ebp, %ebx # %ebx = 加载地址 │ │ │
│ │ │ 149: movl BP_kernel_alignment(%esi), %eax │ │ │
│ │ │ # 从 boot_params.hdr.kernel_alignment 读取对齐要求 │ │ │
│ │ │ # 通常是 0x200000 (2MB) │ │ │
│ │ │ │ │ │
│ │ │ 150-153: 对齐计算 (将 %ebx 对齐到 2MB 边界) │ │ │
│ │ │ decl %eax # %eax = 0x1FFFFF │ │ │
│ │ │ addl %eax, %ebx # %ebx += 0x1FFFFF (向上对齐) │ │ │
│ │ │ notl %eax # %eax = 0xFFE00000 │ │ │
│ │ │ andl %eax, %ebx # %ebx &= 0xFFE00000 │ │ │
│ │ │ # 结果: %ebx 对齐到 2MB 边界 │ │ │
│ │ │ │ │ │
│ │ │ 161: addl BP_init_size(%esi), %ebx │ │ │
│ │ │ # init_size = 内核运行时需要的总内存大小 │ │ │
│ │ │ # %ebx += init_size │ │ │
│ │ │ │ │ │
│ │ │ 162: subl $ rva(_end), %ebx │ │ │
│ │ │ # _end = 压缩内核代码的末尾 │ │ │
│ │ │ # %ebx -= 压缩内核大小 │ │ │
│ │ │ │ │ │
│ │ │ 计算逻辑解释: │ │ │
│ │ │ 解压目标 = (加载地址对齐后) + 内核大小 - 压缩内核大小 │ │ │
│ │ │ 这样确保解压时不会覆盖压缩内核自身 │ │ │
│ │ │ │ │ │
│ │ │ 结果: %ebx = 解压目标地址 (例如 0x1000000) │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ PHASE 4: 建立 4级页表 (行 168-232) │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 目的: 为进入 64位长模式准备页表 │ │ │
│ │ │ │ │ │
│ │ │ 启用 PAE (Physical Address Extension): │ │ │
│ │ │ movl %cr4, %eax │ │ │
│ │ │ orl $X86_CR4_PAE, %eax │ │ │
│ │ │ movl %eax, %cr4 │ │ │
│ │ │ │ │ │
│ │ │ 清零页表区域: │ │ │
│ │ │ leal rva(pgtable)(%ebx), %edi │ │ │
│ │ │ xorl %eax, %eax │ │ │
│ │ │ movl $(BOOT_INIT_PGT_SIZE/4), %ecx │ │ │
│ │ │ rep stosl │ │ │
│ │ │ │ │ │
│ │ │ 建立页表层次结构: │ │ │
│ │ │ Level 4 (PML4): 1 个条目 │ │ │
│ │ │ Level 3 (PDPT): 4 个条目 (映射 4GB) │ │ │
│ │ │ Level 2 (PD): 2048 个条目 (每个 2MB,共 4GB) │ │ │
│ │ │ │ │ │
│ │ │ (详细页表结构见原文档的页表构造部分) │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ PHASE 5: 进入长模式 (行 234-281) │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 235: leal rva(pgtable)(%ebx), %eax │ │ │
│ │ │ 236: movl %eax, %cr3 # 加载页表基址到 CR3 │ │ │
│ │ │ │ │ │
│ │ │ 239-242: 设置 EFER.LME = 1 (启用长模式) │ │ │
│ │ │ movl $MSR_EFER, %ecx # EFER MSR 编号 │ │ │
│ │ │ rdmsr # 读取当前 EFER │ │ │
│ │ │ btsl $_EFER_LME, %eax # 设置 LME 位 │ │ │
│ │ │ wrmsr # 写回 EFER │ │ │
│ │ │ │ │ │
│ │ │ 277-278: 设置 CR0.PG = 1 (启用分页) │ │ │
│ │ │ movl $CR0_STATE, %eax │ │ │
│ │ │ movl %eax, %cr0 │ │ │
│ │ │ # CR0_STATE = PE | MP | ET | NE | WP | AM | PG │ │ │
│ │ │ │ │ │
│ │ │ ★此时进入兼容模式 (IA-32e Compatibility Mode)★ │ │ │
│ │ │ 状态: EFER.LME=1, CR0.PG=1, 但 CS.L=0 │ │ │
│ │ │ │ │ │
│ │ │ 273-274: 准备跳转到 64位代码 │ │ │
│ │ │ leal rva(startup_64)(%ebp), %eax │ │ │
│ │ │ pushl $__KERNEL_CS # 64位代码段选择子 │ │ │
│ │ │ pushl %eax # startup_64 地址 │ │ │
│ │ │ │ │ │
│ │ │ 281: lret # 远返回进入真正64位模式 │ │ │
│ │ │ # 加载 CS = __KERNEL_CS (CS.L=1) │ │ │
│ │ │ # 跳转到 startup_64 │ │ │
│ │ │ │ │ │
│ │ │ ★正式进入 64位长模式 (Long Mode)★ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────┬───────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────────────┐
│ [阶段 4] 压缩内核执行 - arch/x86/boot/compressed/head_64.S │
│ startup_64 (64位长模式,在 0x100000 运行) │
├───────────────────────────────────────────────────────────────────────┤
│ 执行位置: 0x100000 (物理地址) │
│ CPU 状态: 64位长模式 │
│ 寄存器: %rsi = boot_params 物理地址 │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 1. 环境初始化 (行 298-307) │ │
│ │ cld # 清除方向标志 │ │
│ │ cli # 禁用中断 │ │
│ │ 清零所有段寄存器 (64位模式下段基址为0) │ │
│ │ │ │
│ │ 2. 计算解压地址 (行 322-339) │ │
│ │ leaq startup_32(%rip), %rbp # 使用 RIP 相对寻址 │ │
│ │ # 对齐到 2MB 边界并计算解压目标 │ │
│ │ │ │
│ │ 3. 设置栈和 GDT (行 342-374) │ │
│ │ leaq rva(boot_stack_end)(%rbx), %rsp │ │
│ │ 加载 64位 GDT │ │
│ │ │ │
│ │ 4. 加载早期 IDT (行 384) │ │
│ │ call load_stage1_idt │ │
│ │ │ │
│ │ 5. 配置 5级分页 (如果需要,行 415-417) │ │
│ │ movq %r15, %rdi # boot_params │ │
│ │ leaq rva(top_pgtable)(%rbx), %rsi │ │
│ │ call configure_5level_paging │ │
│ │ │ │
│ │ 6. 重定位压缩内核 (行 427-433) │ │
│ │ 目的: 将压缩内核移到缓冲区末尾,避免解压时覆盖 │ │
│ │ leaq (_bss-8)(%rip), %rsi # 源地址 (当前位置) │ │
│ │ leaq rva(_bss-8)(%rbx), %rdi # 目标地址 (新位置) │ │
│ │ std # 设置方向标志 (反向复制) │ │
│ │ rep movsq # 复制 │ │
│ │ cld # 清除方向标志 │ │
│ │ │ │
│ │ 7. 重新加载 GDT 并跳转 (行 440-449) │ │
│ │ leaq rva(gdt64)(%rbx), %rax │ │
│ │ lgdt (%rax) │ │
│ │ leaq rva(.Lrelocated)(%rbx), %rax │ │
│ │ jmp *%rax # 跳转到重定位后的代码 │ │
│ │ │ │
│ │ 8. .Lrelocated: 清零 BSS (行 452-458) │ │
│ │ leaq rva(_bss)(%rbx), %rdi │ │
│ │ leaq rva(_ebss)(%rbx), %rcx │ │
│ │ rep stosb │ │
│ │ │ │
│ │ 9. 初始化恒等映射 (行 462-469) │ │
│ │ pushq %rsi # 保存 boot_params │ │
│ │ leaq rva(_bss)(%rbx), %rdi │ │
│ │ call initialize_identity_maps # 建立恒等映射页表 │ │
│ │ popq %rsi │ │
│ │ │ │
│ │ 10. 解压内核! (行 474-485) ★核心步骤★ │ │
│ │ ┌───────────────────────────────────────────────────────┐ │ │
│ │ │ pushq %rsi # boot_params │ │ │
│ │ │ movq %rsi, %rdi # arg1: boot_params │ │ │
│ │ │ leaq rva(input_data)(%rbx), %rsi # arg2: 压缩数据 │ │ │
│ │ │ movl input_len(%rip), %ecx # arg3: 压缩数据长度 │ │ │
│ │ │ movq %rbp, %r8 # arg4: 输出缓冲区 │ │ │
│ │ │ movl output_len(%rip), %r9d # arg5: 输出长度 │ │ │
│ │ │ │ │ │
│ │ │ call extract_kernel # misc.c::extract_kernel() │ │ │
│ │ │ → 调用解压缩算法 (gzip/bzip2/lzma/xz/lzo/lz4) │ │ │
│ │ │ → 将 vmlinux 解压到目标地址 │ │ │
│ │ │ → 应用 KASLR (如果启用) │ │ │
│ │ │ → 返回解压后内核的入口地址 │ │ │
│ │ │ │ │ │
│ │ │ popq %rsi # 恢复 boot_params │ │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 11. 跳转到解压后的内核 (行 492-495) │ │
│ │ leaq rva(boot_stack_end)(%rbx), %rsp │ │
│ │ popq %rdi # boot_params │ │
│ │ pushq $0 # 返回地址 (不返回) │ │
│ │ jmp *%rax # 跳转到真正的内核入口 │ │
│ │ # %rax = extract_kernel 返回值 │ │
│ │ # 指向 secondary_startup_64 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ★ 压缩内核的使命完成!★ │
│ ★ 真正的内核 (vmlinux) 已解压到目标地址 ★ │
└───────────────────────────────┬───────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────────────┐
│ [阶段 5] 真正的内核执行 - arch/x86/kernel/head_64.S │
│ secondary_startup_64 (64位长模式) │
├───────────────────────────────────────────────────────────────────────┤
│ 执行位置: 0x1000000 或 KASLR 随机地址 (物理地址) │
│ CPU 状态: 64位长模式 │
│ 寄存器: %rsi = boot_params 物理地址 │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 1. 设置数据段和栈 │ │
│ │ 2. 加载内核 GDT/IDT │ │
│ │ 3. 建立最终的页表映射 │ │
│ │ 4. 设置 per-CPU 数据 │ │
│ │ 5. 跳转到 C 代码: x86_64_start_kernel() │ │
│ │ → init/main.c::start_kernel() │ │
│ │ → setup_arch() │ │
│ │ → reserve_real_mode() ★重新使用 0x90000★ │ │
│ │ → rest_init() │ │
│ │ → kernel_init() (PID 1) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ★ 内核完全启动!★ │
└───────────────────────────────────────────────────────────────────────┘
内存图
物理内存布局 (启动阶段):
0x00000000 ┌─────────────────────────────────────────┐
│ BIOS 数据区域 (BDA) │
0x00000500 ├─────────────────────────────────────────┤
│ 实模式代码/数据 │
0x00007C00 ├─────────────────────────────────────────┤
│ Bootloader (512字节) │
0x00010000 ├─────────────────────────────────────────┤
│ boot_params (zeropage, 4KB) │ <- %esi 指向这里
0x00011000 ├─────────────────────────────────────────┤
│ 命令行参数 │
0x00090000 ├─────────────────────────────────────────┤
│ 实模式代码段 │
0x00100000 ├═════════════════════════════════════════┤
(1MB) ║ 压缩内核 vmlinuz ║ <- 典型加载位置
║ ┌─────────────────────────────┐ ║
║ │ startup_32 (入口点) │ ║
║ │ startup_64 │ ║
║ │ 压缩数据 │ ║
║ │ boot_stack (16KB) │ ║
║ │ boot_heap │ ║
║ └─────────────────────────────┘ ║
0x01000000 ├─────────────────────────────────────────┤
│ (示例: %ebp 加载地址) │
│ │
0x02000000 ├═════════════════════════════════════════┤
(示例) ║ 解压目标区域 (计算的 %ebx) ║
║ ┌─────────────────────────────┐ ║
║ │ pgtable (24KB) │ ║ <- 页表
║ │ PML4 (4KB) │ ║
║ │ PDPT (4KB) │ ║
║ │ PD×4 (16KB) │ ║
║ ├─────────────────────────────┤ ║
║ │ 解压后的内核 vmlinux │ ║
║ │ .text (代码段) │ ║
║ │ .data (数据段) │ ║
║ │ .bss (未初始化数据) │ ║
║ └─────────────────────────────┘ ║
0x???????? ├─────────────────────────────────────────┤
│ initrd (如果存在) │
... │ │
0xFFFFFFFF └─────────────────────────────────────────┘
(4GB)
实模式与保护模式
Real Address Mode (实模式):在此模式下地址访问的是真实地内存地址所在位置。
Protected Address Mode (保护模式):采用虚拟内存,页等机制对内存进行保护。
BIOS 阶段
CPU状态
电源接通时,CPU的寄存器值为:
CS : 0xfffff000 即此时代码执行的地址是0xfffffff0 (BIOS程序所在的ROM区域)
- CS selector = 0xf000
- CS base = 0xffff0000
BIOS 启动
BIOS 的固件程序会将硬盘启动区 512 B的数据原封不动复制给 0x7c00 这个位置,并且跳转到 0x7c00
最后两字节也就是检查第一扇区的最后两字节 boot Signature 和 Magic Number 分别是否为0x55 和 0xAA (01010101 10101010这种交叉数据最容易检查传输是否错误)
或许你会疑惑为什么是 0x7c00 ,这里可以看一下这个文章:https://blog.csdn.net/hbuxiaofei/article/details/134587909
Bootloader阶段
当 BIOS 执行完毕,就会将 bootloader 复制给 0x7c00 处
并且 BIOS 会将控制权给 bootloader
在早期版本下 bootloader 一般是 Linux 的 boot/bootsect.s 中定义的
在后面一般 bootloader 单独存在的软件
对于 x86/x86-64 架构:
- GRUB2 (Grand Unified Bootloader 2) - 最常用
- systemd-boot (以前叫 gummiboot) - 简单轻量
- rEFInd - 主要用于 UEFI 系统
- SYSLINUX/ISOLINUX - 用于 Live CD/USB
对于 ARM 架构:
- U-Boot (Das U-Boot) - 嵌入式系统主流
- GRUB2 - 也在一些 ARM 设备上使用
GRUB
一般来说 GRUB 是最常见的 bootloader,其实 bootloader 干的事情基本一致,所以我们主要分析 GRUB
下文主要是引用的这个文章感兴趣可以直接看原文:https://xinqiu.gitbooks.io/linux-insides-cn/content/Booting/linux-bootstrap-1.html
grub_main 初始化控制台,计算模块基地址,设置 root 设备,读取 grub 配置文件,加载模块。最后,将 GRUB 置于 normal 模式,在这个模式中,grub_normal_execute (from grub-core/normal/main.c) 将被调用以完成最后的准备工作,然后显示一个菜单列出所用可用的操作系统。(这个界面我们就比较熟悉了对吧,比如进入网吧我们就会看见这个界面)
内存布局
- 经过引导后,内存界面大概就是下方所示(对于具有启动协议版本 >= 2.02 的现代 bzImage 内核,一般使用的是下方的内存布局),kernel 的 setup 程序(realmode code)加载到了内存中
- 当是 bzImage 时,保护模式的下的内核被重定位到 0x100000
| Protected-mode kernel |
100000 +------------------------+
| I/O memory hole |
0A0000 +------------------------+
| Reserved for BIOS | Leave as much as possible unused
~ ~
| Command line | (Can also be below the X+10000 mark)
X+10000 +------------------------+
| Stack/heap | For use by the kernel real-mode code.
X+08000 +------------------------+
| Kernel setup | The kernel real-mode code.
| Kernel boot sector | The kernel legacy boot sector.
X +------------------------+
| Boot loader | <- Boot sector entry point 0x7C00
001000 +------------------------+
| Reserved for MBR/BIOS |
000800 +------------------------+
| Typically used by MBR |
000600 +------------------------+
| BIOS use only |
000000 +------------------------+
- 内核加载器的传统内存映射,用于 Image 或 zImage 内核,通常如下所示
| |
0A0000 +------------------------+
| Reserved for BIOS | Do not use. Reserved for BIOS EBDA.
09A000 +------------------------+
| Command line |
| Stack/heap | For use by the kernel real-mode code.
098000 +------------------------+
| Kernel setup | The kernel real-mode code.
090200 +------------------------+
| Kernel boot sector | The kernel legacy boot sector.
090000 +------------------------+
| Protected-mode kernel | The bulk of the kernel image.
010000 +------------------------+
| Boot loader | <- Boot sector entry point 0000:7C00
001000 +------------------------+
| Reserved for MBR/BIOS |
000800 +------------------------+
| Typically used by MBR |
000600 +------------------------+
| BIOS use only |
000000 +------------------------+
-
根据 arch/x86/boot/header.S 定义,可以知道 setup 一般在 0x90000,code32_start一般在 0x100000
-
setup_move_size: .word 0x8000 # size to move, when setup is not
# loaded at 0x90000. We will move setup
# to 0x90000 then just before jumping
# into the kernel. However, only the
# loader knows how much data behind
# us also needs to be loaded.
code32_start: # here loaders can put a different
start address for 32-bit code.
.long 0x100000 # 0x100000 = default for big kernel
-
总结
-
┌─────────────────────────────────────────────────────────────┐
│ [时刻1] Bootloader (GRUB) 加载阶段 │
├─────────────────────────────────────────────────────────────┤
│ │
│ bzImage 文件结构: │
│ ┌──────────────────────┐ │
│ │ Setup.bin (32KB) │ ← header.S, main.c, pm.c 编译的 │
│ ├──────────────────────┤ │
│ │ vmlinux.bin.gz │ ← head_64.S + 压缩的内核 │
│ └──────────────────────┘ │
│ │
│ Bootloader 的操作: │
│ 1. 读取 Setup.bin → 加载到物理地址 0x90000 │
│ 2. 读取 vmlinux.bin.gz → 加载到物理地址 0x100000 (1MB) │
│ 3. 跳转到 0x90000 开始执行 (header.S::_start) │
│ │
│ 此时内存布局: │
│ 0x90000: ★Setup代码★ (header.S, main.c, pm.c...) │
│ 0x100000: ★压缩内核★ (head_64.S::startup_32) │
└─────────────────────────────────────────────────────────────┘
Setup 阶段
当 bootloader 完成工作后,我们的 kernel setup代码(realmode)就被加载进入了内存中了
然后 bootloader 会将执行权限交给 kernel (也就是 arch/x86/boot/header.S _start 函数开始执行)
vmlinuz是压缩后的kernel,于是可以将它分为两部分
setup.bin 程序的入口为__start
OBJCOPYFLAGS_setup.bin := -O binary
$(obj)/setup.bin: $(obj)/setup.elf FORCE$(call if_changed,objcopy)LDFLAGS_setup.elf := -T
$(obj)/setup.elf: $(src)/setup.ld $(SETUP_OBJS) FORCE$(call if_changed,ld)SETUP_OBJS = $(addprefix $(obj)/,$(setup-y))setup-y += a20.o bioscall.o cmdline.o copy.o cpu.o cpucheck.o edd.o
setup-y += header.o main.o mca.o memory.o pm.o pmjump.o
setup-y += printf.o regs.o string.o tty.o video.o video-mode.o
setup-y += version.o
setup-$(CONFIG_X86_APM_BOOT) += apm.o
setup-y += video-vga.o
setup-y += video-vesa.o
setup-y += video-bios.o
__start
执行 arch/x86/boot/header.S __start代码
在 _start 函数开始之前,还有很多的代码,一般这些代码就是 kenerl 自带的 bootloader (现在已经不再使用,只是会输出一些错误信息)
.globl _start
_start:
# Explicitly enter this as bytes, or the assembler
# tries to generate a 3-byte jump here, which causes
# everything else to push off to the wrong offset.
.byte 0xeb # short (2-byte) jump
.byte start_of_setup-1f
start_of_setup
这里算得上 Linux 真正执行的第一个代码
主要做的事情就是:
初始化段寄存器、设置堆栈、代码段规范化、验证设置签名、清空BSS段
然后就是跳转到 main 函数(arch/x86/boot/main.c)
.section ".entrytext", "ax"
start_of_setup:
# Force %es = %ds
movw %ds, %ax
movw %ax, %es
cld
movw %ss, %dx
cmpw %ax, %dx # %ds == %ss?
movw %sp, %dx
je 2f # -> assume %sp is reasonably set
# Invalid %ss, make up a new stack
movw $_end, %dx
testb $CAN_USE_HEAP, loadflags
jz 1f
movw heap_end_ptr, %dx
1: addw $STACK_SIZE, %dx
jnc 2f
xorw %dx, %dx # Prevent wraparound
2: # Now %dx should point to the end of our stack space
andw $~3, %dx # dword align (might as well...)
jnz 3f
movw $0xfffc, %dx # Make sure we're not zero
3: movw %ax, %ss
movzwl %dx, %esp # Clear upper half of %esp
sti # Now we should have a working stack
# We will have entered with %cs = %ds+0x20, normalize %cs so
# it is on par with the other segments.
pushw %ds
pushw $6f
lretw
6:
# Check signature at end of setup
cmpl $0x5a5aaa55, setup_sig
jne setup_bad
# Zero the bss
movw $__bss_start, %di
movw $_end+3, %cx
xorl %eax, %eax
subw %di, %cx
shrw $2, %cx
rep; stosl
# Jump to C code (should not return)
calll main
在初始化后各个段寄存器的值应该是
- CS = DS (通过
lretw技巧实现)
- DS = ES = SS = 原始DS值(应该是0x1000)
- 所有段寄存器现在都有相同的值
Call Main function
start_of_setup 设置了一些寄存器初始化后,就跳转到了 main 函数 (arch/x86/boot/main.c)
主要是完成一些初始化操作后,进入 Protect Mode
void main(void)
{
// 初始化默认I/O操作
init_default_io_ops();
// 启动协议块拷贝到 boot_params 的hdr字段
/* First, copy the boot header into the "zeropage" */
copy_boot_params();
/* Initialize the early-boot console */
console_init();
if (cmdline_find_option_bool("debug"))
puts("early console in setup code\n");
/* End of heap check */
// 初始化全局堆
init_heap();
// 确保 CPU 正在运行在正确的特权级上
/* Make sure we have all the proper CPU support */
if (validate_cpu()) {
puts("Unable to boot - please use a kernel appropriate for your CPU.\n");
die();
}
/* Tell the BIOS what CPU mode we intend to run in */
set_bios_mode();
// 从 BIOS 获取内存布局信息
/* Detect memory layout */
detect_memory();
// 键盘初始化,设置按键检测频率
/* Set keyboard repeat rate (why?) and query the lock flags */
keyboard_init();
/* Query Intel SpeedStep (IST) information */
query_ist();
/* Query APM information */
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
query_apm_bios();
#endif
/* Query EDD information */
#if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
query_edd();
#endif
/* Set the video mode */
set_video();
// 转移到保护模式
/* Do the last things and invoke protected mode */
go_to_protected_mode();
}
copy_boot_params
在正式切换到 protected mode 之前的内存准备工作,主要是将内核的 struct setup_header hdr信息拷贝到 boot_params 中 hdr 去
/*
* Copy the header into the boot parameter block. Since this
* screws up the old-style command line protocol, adjust by
* filling in the new-style command line pointer instead.
*/
static void copy_boot_params(void)
{
struct old_cmdline {
u16 cl_magic;
u16 cl_offset;
};
const struct old_cmdline * const oldcmd = absolute_pointer(OLD_CL_ADDRESS);
BUILD_BUG_ON(sizeof(boot_params) != 4096);
memcpy(&boot_params.hdr, &hdr, sizeof(hdr));
if (!boot_params.hdr.cmd_line_ptr && oldcmd->cl_magic == OLD_CL_MAGIC) {
/* Old-style command line protocol */
u16 cmdline_seg;
/*
* Figure out if the command line falls in the region
* of memory that an old kernel would have copied up
* to 0x90000...
*/
if (oldcmd->cl_offset < boot_params.hdr.setup_move_size)
cmdline_seg = ds();
else
cmdline_seg = 0x9000;
boot_params.hdr.cmd_line_ptr = (cmdline_seg << 4) + oldcmd->cl_offset;
}
}
boot_params
引导加载程序(如GRUB)与Linux内核之间传递启动参数的关键数据结构。
- 使用固定偏移量,确保引导加载程序和内核对字段位置有统一理解
/* The so-called "zeropage" */
struct boot_params {
struct screen_info screen_info; /* 0x000 */
struct apm_bios_info apm_bios_info; /* 0x040 */
__u8 _pad2[4]; /* 0x054 */
__u64 tboot_addr; /* 0x058 */
struct ist_info ist_info; /* 0x060 */
__u64 acpi_rsdp_addr; /* 0x070 */
__u8 _pad3[8]; /* 0x078 */
__u8 hd0_info[16]; /* obsolete! */ /* 0x080 */
__u8 hd1_info[16]; /* obsolete! */ /* 0x090 */
struct sys_desc_table sys_desc_table; /* obsolete! */ /* 0x0a0 */
struct olpc_ofw_header olpc_ofw_header; /* 0x0b0 */
__u32 ext_ramdisk_image; /* 0x0c0 */
__u32 ext_ramdisk_size; /* 0x0c4 */
__u32 ext_cmd_line_ptr; /* 0x0c8 */
__u8 _pad4[112]; /* 0x0cc */
__u32 cc_blob_address; /* 0x13c */
struct edid_info edid_info; /* 0x140 */
struct efi_info efi_info; /* 0x1c0 */
__u32 alt_mem_k; /* 0x1e0 */
__u32 scratch; /* Scratch field! */ /* 0x1e4 */
__u8 e820_entries; /* 0x1e8 */
__u8 eddbuf_entries; /* 0x1e9 */
__u8 edd_mbr_sig_buf_entries; /* 0x1ea */
__u8 kbd_status; /* 0x1eb */
__u8 secure_boot; /* 0x1ec */
__u8 _pad5[2]; /* 0x1ed */
/*
* The sentinel is set to a nonzero value (0xff) in header.S.
*
* A bootloader is supposed to only take setup_header and put
* it into a clean boot_params buffer. If it turns out that
* it is clumsy or too generous with the buffer, it most
* probably will pick up the sentinel variable too. The fact
* that this variable then is still 0xff will let kernel
* know that some variables in boot_params are invalid and
* kernel should zero out certain portions of boot_params.
*/
__u8 sentinel; /* 0x1ef */
__u8 _pad6[1]; /* 0x1f0 */
struct setup_header hdr; /* setup header */ /* 0x1f1 */
__u8 _pad7[0x290-0x1f1-sizeof(struct setup_header)];
__u32 edd_mbr_sig_buffer[EDD_MBR_SIG_MAX]; /* 0x290 */
struct boot_e820_entry e820_table[E820_MAX_ENTRIES_ZEROPAGE]; /* 0x2d0 */
__u8 _pad8[48]; /* 0xcd0 */
struct edd_info eddbuf[EDDMAXNR]; /* 0xd00 */
__u8 _pad9[276]; /* 0xeec */
} __attribute__((packed));
-
boot_params 的三次传递
-
┌─────────────────────────────────────────────────────────────────┐
│ boot_params 的生命周期和传递过程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [传递1] Bootloader → Setup 代码 │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ 时机: Bootloader 加载完 bzImage 后 │ │
│ │ │ │
│ │ 操作: │ │
│ │ 1. Bootloader 分配 4KB 内存 (称为 zeropage) │ │
│ │ 位置: 通常在 0x10000 附近 │ │
│ │ │ │
│ │ 2. Bootloader 填充信息: │ │
│ │ - 从 bzImage 的 header.S 读取 setup_header │ │
│ │ - 填充内存映射 (E820) │ │
│ │ - 填充命令行参数位置 │ │
│ │ - 填充 initrd 信息 │ │
│ │ │ │
│ │ 3. 将地址传递给内核: │ │
│ │ %esi = boot_params 物理地址 │ │
│ │ │ │
│ │ 4. 跳转到 0x90000 (Setup 代码) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ [传递2] Setup 代码内部拷贝 │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ 位置: arch/x86/boot/main.c::copy_boot_params() │ │
│ │ 时机: main() 函数的第一步 │ │
│ │ │ │
│ │ 源地址 1: &hdr (header.S 中定义的 setup_header) │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ .globl hdr │ │ │
│ │ │ hdr: │ │ │
│ │ │ .byte setup_sects - 1 │ │ │
│ │ │ .word ROOT_RDONLY │ │ │
│ │ │ ... │ │ │
│ │ │ code32_start: │ │ │
│ │ │ .long 0x100000 │ │ │
│ │ │ ... │ │ │
│ │ │ │ │ │
│ │ │ 物理地址: 0x90000 + 0x1F1 ≈ 0x901F1 │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 源地址 2: Bootloader 传递的 boot_params (%esi) │ │
│ │ - 包含 Bootloader 填充的额外信息 │ │
│ │ - E820 内存映射、命令行等 │ │
│ │ │ │
│ │ 目标地址: &boot_params (BSS 段全局变量) │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ // arch/x86/boot/main.c │ │ │
│ │ │ struct boot_params boot_params __attribute__( │ │ │
│ │ │ (aligned(16))); │ │ │
│ │ │ │ │ │
│ │ │ 位于 Setup 代码的 BSS 段 │ │ │
│ │ │ 物理地址: 0x90000 + offset │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 拷贝操作: │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ void copy_boot_params(void) { │ │ │
│ │ │ // 第一步: 复制 header.S 中的 hdr │ │ │
│ │ │ memcpy(&boot_params.hdr, │ │ │
│ │ │ &hdr, │ │ │
│ │ │ sizeof(hdr)); │ │ │
│ │ │ // 此时 boot_params 有了完整的 setup_header │ │ │
│ │ │ │ │ │
│ │ │ // 第二步: 处理命令行 (如果有) │ │ │
│ │ │ if (!boot_params.hdr.cmd_line_ptr && ...) { │ │ │
│ │ │ // 兼容老式 bootloader │ │ │
│ │ │ } │ │ │
│ │ │ } │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 结果: boot_params 现在包含: │ │
│ │ - header.S 中的所有配置字段 │ │
│ │ - code32_start = 0x100000 │ │
│ │ - Bootloader 填充的运行时信息 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ [传递3] Setup 代码 → 压缩内核 │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ 位置: arch/x86/boot/pmjump.S::protected_mode_jump() │ │
│ │ │ │
│ │ protected_mode_jump( │ │
│ │ boot_params.hdr.code32_start, // = 0x100000 │ │
│ │ (u32)&boot_params + (ds() << 4) // 物理地址 │ │
│ │ ); │ │
│ │ │ │
│ │ 在 protected_mode_jump 中: │ │
│ │ movl %edx, %esi # %esi = &boot_params (物理地址) │ │
│ │ ... │ │
│ │ jmpl *%eax # 跳转到 0x100000 │ │
│ │ # %esi 寄存器保持不变 │ │
│ │ │ │
│ │ ★ %esi 一直保持着 boot_params 的物理地址 ★ │ │
│ │ ★ 传递给压缩内核和真正的内核 ★ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ [传递4] 压缩内核 → 真正的内核 │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ %esi 在整个压缩内核执行期间保持不变 │ │
│ │ │ │
│ │ 在 startup_32, startup_64, extract_kernel 中都使用 %esi │ │
│ │ │ │
│ │ 最后跳转到真正内核时: │ │
│ │ movq %rsi, %rdi # 传递给 secondary_startup_64 │ │
│ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
struct setup_header {
__u8 setup_sects;
__u16 root_flags;
__u32 syssize;
__u16 ram_size;
__u16 vid_mode;
__u16 root_dev;
__u16 boot_flag;
__u16 jump;
__u32 header;
__u16 version;
__u32 realmode_swtch;
__u16 start_sys_seg;
__u16 kernel_version;
__u8 type_of_loader;
__u8 loadflags;
__u16 setup_move_size;
__u32 code32_start;
__u32 ramdisk_image;
__u32 ramdisk_size;
__u32 bootsect_kludge;
__u16 heap_end_ptr;
__u8 ext_loader_ver;
__u8 ext_loader_type;
__u32 cmd_line_ptr;
__u32 initrd_addr_max;
__u32 kernel_alignment;
__u8 relocatable_kernel;
__u8 min_alignment;
__u16 xloadflags;
__u32 cmdline_size;
__u32 hardware_subarch;
__u64 hardware_subarch_data;
__u32 payload_offset;
__u32 payload_length;
__u64 setup_data;
__u64 pref_address;
__u32 init_size;
__u32 handover_offset;
__u32 kernel_info_offset;
} __attribute__((packed));
go_to_protected_mode(切换保护模式)
/*
* Actual invocation sequence
*/
void go_to_protected_mode(void)
{
/* Hook before leaving real mode, also disables interrupts */
realmode_switch_hook();
// 开启 A20 地址线,此时有能力访问所有的地址空间
/* Enable the A20 gate */
if (enable_a20()) {
puts("A20 gate not responding, unable to boot...\n");
die();
}
// 重置处理器
/* Reset coprocessor (IGNNE#) */
reset_coprocessor();
/* Mask all interrupts in the PIC */
mask_all_interrupts();
// 设置 IDT 和 GDT
/* Actual transition to protected mode... */
setup_idt();
setup_gdt();
/* 跳转到code32_start处,它的地址位于启动协议块的头部 */
protected_mode_jump(boot_params.hdr.code32_start,
(u32)&boot_params + (ds() << 4));
}
protected_mode_jump
结合参数protected_mode_jump(boot_params.hdr.code32_start,(u32)&boot_params + (ds() << 4)); 一起分析
传参规范(传统寄存器传参):
参数1 → %eax
参数2 → %edx
参数3 → %ebx
参数4 → %ecx
参数5 → %esi
参数6 → %edi
SYM_FUNC_START_NOALIGN(protected_mode_jump)
// boot_params 保存给 esi
movl %edx, %esi # Pointer to boot_params table
// ebx 清空
xorl %ebx, %ebx
// 获取当前的 CS 段(realmode)
movw %cs, %bx
// 转化为线性地址 cs << 4 (cs * 16)
shll $4, %ebx
//
addl %ebx, 2f
jmp 1f # Short jump to serialize on 386/486
1:
// 数据段选择子 GDT 第三个描述符 (数据段)
movw $__BOOT_DS, %cx
// TSS 选择子 GDT 第四个描述符 (任务状态段)
movw $__BOOT_TSS, %di
// 设置保护模式 bit,进入保护模式
movl %cr0, %edx
orb $X86_CR0_PE, %dl # Protected mode
movl %edx, %cr0
# Transition to 32-bit mode
.byte 0x66, 0xea # ljmpl opcode
2: .long .Lin_pm32 # offset
.word __BOOT_CS # segment
SYM_FUNC_END(protected_mode_jump)
.code32
.section ".text32","ax"
SYM_FUNC_START_LOCAL_NOALIGN(.Lin_pm32)
# Set up data segments for flat 32-bit mode
// 设置数据段寄存器
movl %ecx, %ds
movl %ecx, %es
movl %ecx, %fs
movl %ecx, %gs
movl %ecx, %ss
# The 32-bit code sets up its own stack, but this way we do have
# a valid stack if some debugging hack wants to use it.
addl %ebx, %esp
# Set up TR to make Intel VT happy
ltr %di
# Clear registers to allow for future extensions to the
# 32-bit boot protocol
xorl %ecx, %ecx
xorl %edx, %edx
xorl %ebx, %ebx
xorl %ebp, %ebp
xorl %edi, %edi
# Set up LDTR to make Intel VT happy
lldt %cx
# 跳转到 32位入口
jmpl *%eax # Jump to the 32-bit entrypoint
SYM_FUNC_END(.Lin_pm32)
protected_mode_jump 被调用
↓
计算线性地址,准备段选择子
↓
设置CR0.PE=1,进入保护模式
↓
远跳转到 .Lin_pm32 (刷新流水线)
↓
保护模式 (32位) - .Lin_pm32
↓
初始化段寄存器,设置堆栈
↓
设置系统表(TR, LDTR)
↓
清理寄存器状态
↓
跳转到 code32_start (arch/x86/boot/compressed/head_64.S)
压缩内核 阶段
code32_start
- code32_start 的地址一般在 0x100000
setup_move_size: .word 0x8000 # size to move, when setup is not
# loaded at 0x90000. We will move setup
# to 0x90000 then just before jumping
# into the kernel. However, only the
# loader knows how much data behind
# us also needs to be loaded.
code32_start: # here loaders can put a different
# start address for 32-bit code.
.long 0x100000 # 0x100000 = default for big kernel
调用链
1. Bootloader (GRUB)
↓
2. arch/x86/boot/main.c (实模式)
↓
3. arch/x86/boot/pm.c::go_to_protected_mode() (进入保护模式)
↓
4. 【当前位置】arch/x86/boot/compressed/head_64.S::startup_32 (压缩内核入口)
↓
5. arch/x86/boot/compressed/misc.c::extract_kernel() (解压内核)
↓
6. arch/x86/kernel/head_64.S::secondary_startup_64 (真正的内核入口)
↓
7. start_kernel() (C代码,内核主初始化)
初始化和地址计算
SYM_FUNC_START(startup_32)
cld
cli
leal (BP_scratch+4)(%esi), %esp
call 1f
1: popl %ebp
subl $ rva(1b), %ebp
- cld/cli: 清除方向标志,禁用中断
- 地址重定位计算:通过 call/pop 技巧获取当前运行地址
- call 1f 将返回地址压栈
- popl %ebp 获取实际运行地址
- subl $ rva(1b), %ebp 计算与编译地址的差值
- %ebp 现在包含实际加载地址
设置 GDT 和段寄存器
/* Load new GDT with the 64bit segments using 32bit descriptor */
leal rva(gdt)(%ebp), %eax
movl %eax, 2(%eax)
lgdt (%eax)
/* Load segment registers with our descriptors */
movl $__BOOT_DS, %eax
movl %eax, %ds
movl %eax, %es
movl %eax, %fs
movl %eax, %gs
movl %eax, %ss
/* Setup a stack and load CS from current GDT */
leal rva(boot_stack_end)(%ebp), %esp
pushl $__KERNEL32_CS
leal rva(1f)(%ebp), %eax
pushl %eax
lretl
1:
- 加载包含 64 位段描述符的新 GDT
- 设置所有段寄存器为平坦模式
- 设置栈指针
- 通过 lretl 远返回加载新的 CS
验证CPU和计算解压目标地址
/* Make sure cpu supports long mode. */
call verify_cpu
testl %eax, %eax
jnz .Lno_longmode
/*
* Compute the delta between where we were compiled to run at
* and where the code will actually run at.
*
* %ebp contains the address we are loaded at by the boot loader and %ebx
* contains the address where we should move the kernel image temporarily
* for safe in-place decompression.
*/
#ifdef CONFIG_RELOCATABLE
movl %ebp, %ebx
movl BP_kernel_alignment(%esi), %eax
decl %eax
addl %eax, %ebx
notl %eax
andl %eax, %ebx
cmpl $LOAD_PHYSICAL_ADDR, %ebx
jae 1f
#endif
movl $LOAD_PHYSICAL_ADDR, %ebx
1:
/* Target address to relocate to for decompression */
addl BP_init_size(%esi), %ebx
subl $ rva(_end), %ebx
- 验证 CPU 是否支持 64 位长模式
- 计算内核解压目标地址:
- 根据 kernel_alignment 对齐 %ebx
- 确保地址安全(避免覆盖压缩内核)
建立早期页表
- 启用 PAE 模式(CR4.PAE)
- 清零页表区域
- Level 4 (PML4):根页表,1 个条目
- Level 3 (PDPT):页目录指针表,4 个条目
- Level 2 (PD):页目录,2048 个条目,每个映射 2MB
- 总共映射 4GB 内存(2048 × 2MB = 4GB)
- 使用 大页(2MB) 映射(标志 0x183 = Present + RW + PS)
页表结构
物理内存布局 (4级分页):
┌──────────────────────────────────────────────────────────┐
│ Level 4 (PML4) │
│ 位置: pgtable + 0x0 │
│ 大小: 4KB (512条目) │
├──────────────────────────────────────────────────────────┤
│ PML4[0] = pgtable + 0x1000 + 0x07 (Present+RW+User) │
│ PML4[1-511] = 0 (未使用) │
└──────────┬───────────────────────────────────────────────┘
│
└──────┐
↓
┌──────────────────────────────────────────────────────────┐
│ Level 3 (PDPT - 页目录指针表) │
│ 位置: pgtable + 0x1000 │
│ 大小: 4KB (512条目) │
├──────────────────────────────────────────────────────────┤
│ PDPT[0] = pgtable + 0x2000 + 0x07 │
│ PDPT[1] = pgtable + 0x3000 + 0x07 │
│ PDPT[2] = pgtable + 0x4000 + 0x07 │
│ PDPT[3] = pgtable + 0x5000 + 0x07 │
│ PDPT[4-511] = 0 (未使用) │
└──┬───┬───┬───┬───────────────────────────────────────────┘
│ │ │ │
└┐ └┐ └┐ └┐
↓ ↓ ↓ ↓
┌──────────────────────────────────────────────────────────┐
│ Level 2 (PD - 页目录) × 4 │
│ 位置: pgtable + 0x2000 ~ 0x5000 │
│ 每个大小: 4KB (512条目) │
├──────────────────────────────────────────────────────────┤
│ PD[0][0] = 0x00000000 + 0x183 (Present+RW+PS+User) │
│ PD[0][1] = 0x00200000 + 0x183 (2MB) │
│ PD[0][2] = 0x00400000 + 0x183 (4MB) │
│ ... │
│ PD[0][511] = 0x3FE00000 + 0x183 (1GB - 2MB) │
│ │
│ PD[1][0-511] = 映射 1GB ~ 2GB │
│ PD[2][0-511] = 映射 2GB ~ 3GB │
│ PD[3][0-511] = 映射 3GB ~ 4GB │
│ │
│ 总共: 2048 条目 × 2MB = 4GB │
└──────────────────────────────────────────────────────────┘
↓ (使用 2MB 大页,无 Level 1)
┌──────────────────────────────────────────────────────────┐
│ 物理内存 0x00000000 ~ 0xFFFFFFFF │
│ (4GB) │
└──────────────────────────────────────────────────────────┘
建立 Level4 (PML4)
Page Map Level 4
/* Build Level 4 */
// edi = pgtable 物理地址
leal rva(pgtable + 0)(%ebx), %edi
// eax = edi + 0x1007 -> 设置 P RW U/S 12+ bit位
// eax 为 level 3 表,并设置了标志位
leal 0x1007 (%edi), %eax
// 写入 PML4[0] 高低位为0
movl %eax, 0(%edi)
addl %edx, 4(%edi)
- 内存构造
- 也就是说 PML4[0]存储的 level 3的指针
PML4 表 (位于 pgtable + 0):
Offset | Value
--------|---------------------------------
0x0000 | pgtable + 0x1007 (低32位)
0x0004 | 0 或加密掩码 (高32位)
0x0008 | 0 (PML4[1])
... | 0 (PML4[2-511])
建立 level3 (PDPT)
Page Directory Pointer Table
// edi 为 PDPT 表
leal rva(pgtable + 0x1000)(%ebx), %edi
// eax 指向 level2 (pagetable + 0x2007)
leal 0x1007(%edi), %eax
// 4 PDPT 条目 4 * 1GB = 4GB space
movl $4, %ecx
// 循环遍历指针填入
1: movl %eax, 0x00(%edi)
addl %edx, 0x04(%edi)
addl $0x00001000, %eax
addl $8, %edi
decl %ecx
jnz 1b
PDPT 表 (位于 pgtable + 0x1000):
Offset | Value | 映射范围
--------|--------------------------|----------
0x0000 | pgtable + 0x2007 (低32) | 0-1GB
0x0004 | 加密掩码 (高32) |
0x0008 | pgtable + 0x3007 | 1-2GB
0x000C | 加密掩码 |
0x0010 | pgtable + 0x4007 | 2-3GB
0x0014 | 加密掩码 |
0x0018 | pgtable + 0x5007 | 3-4GB
0x001C | 加密掩码 |
建立 level2 (PD)
/* Build Level 2 */
// 获取第一个 PD 表起始地址
leal rva(pgtable + 0x2000)(%ebx), %edi
movl $0x00000183, %eax
// 循环 2048 次,每条目 2MB 2048 * 2 = 4 GB
movl $2048, %ecx
// pd[0][index] 低位= 0x183
1: movl %eax, 0(%edi)
// pd[0][index] 高位 = 加密掩码
addl %edx, 4(%edi)
// eax += 0x200000
addl $0x00200000, %eax
// 移动下一个条目
addl $8, %edi
decl %ecx
jnz 1b
- 0x183的标识位解析
- 0x183 = 0001 1000 0011 (二进制)
- Bit 0 (P): Present = 1 (页存在)
- Bit 1 (RW): Read/Write = 1 (可写)
- Bit 2 (U/S): User/Supervisor = 0 (内核)
- Bit 7 (PS): Page Size = 1 (大页,2MB)
- Bit 8 (G): Global = 1 (全局页)
- Bits 21-31: 物理地址高位 = 0
- 内存构造
启动长模式
/* Enable the boot page tables */
leal rva(pgtable)(%ebx), %eax
movl %eax, %cr3
/* Enable Long mode in EFER (Extended Feature Enable Register) */
movl $MSR_EFER, %ecx
rdmsr
btsl $_EFER_LME, %eax
wrmsr
/* After gdt is loaded */
xorl %eax, %eax
lldt %ax
movl $__BOOT_TSS, %eax
ltr %ax
#ifdef CONFIG_AMD_MEM_ENCRYPT
/* Check if the C-bit position is correct when SEV is active */
call startup32_check_sev_cbit
#endif
/*
* Setup for the jump to 64bit mode
*
* When the jump is performed we will be in long mode but
* in 32bit compatibility mode with EFER.LME = 1, CS.L = 0, CS.D = 1
* (and in turn EFER.LMA = 1). To jump into 64bit mode we use
* the new gdt/idt that has __KERNEL_CS with CS.L = 1.
* We place all of the values on our mini stack so lret can
* used to perform that far jump.
*/
leal rva(startup_64)(%ebp), %eax
#ifdef CONFIG_EFI_MIXED
cmpb $1, rva(efi_is64)(%ebp)
je 1f
leal rva(startup_64_mixed_mode)(%ebp), %eax
1:
#endif
pushl $__KERNEL_CS
pushl %eax
/* Enter paged protected Mode, activating Long Mode */
movl $CR0_STATE, %eax
movl %eax, %cr0
/* Jump from 32bit compatibility mode into 64bit mode. */
lret
- 加载 CR3:启用页表
- 设置 EFER.LME:启用长模式(Long Mode Enable)
- 设置 CR0.PG:启用分页
- 此时进入 兼容模式(Long Mode Compatibility)
- EFER.LME=1, CS.L=0, CS.D=1
- lret 远返回:跳转到 64 位代码段
startup_64 (解压内核)
核心功能主要是完成了内核的解压,随后跳转到解压后的内核
流程
startup_64 执行流程:
┌─────────────────────────────────────────────────────────────┐
│ 入口: startup_64 (偏移 0x200) │
│ 来源: 从 startup_32 通过 lret 进入 OR 64位 bootloader │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ PHASE 1: 初始化环境 (行 298-307) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 298: cld # 清除方向标志 │ │
│ │ 299: cli # 禁用中断 │ │
│ │ 302-307: 清零所有段寄存器 (ds, es, ss, fs, gs) │ │
│ │ 64位模式下段基址为0,使用平坦内存模型 │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ PHASE 2: 计算解压地址 (行 322-339) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 324: leaq startup_32(%rip), %rbp │ │
│ │ # 使用 RIP 相对寻址获取当前位置 │ │
│ │ # %rbp = 当前代码的物理地址 │ │
│ │ │ │
│ │ 325-329: 对齐计算 (与 startup_32 中类似) │ │
│ │ movl BP_kernel_alignment(%rsi), %eax │ │
│ │ # 从 boot_params 读取对齐要求 (2MB) │ │
│ │ decl %eax # %eax = 0x1FFFFF │ │
│ │ addq %rax, %rbp # 向上对齐 │ │
│ │ notq %rax # %rax = 0xFFFFFFFFFFE00000 │ │
│ │ andq %rax, %rbp # 清除低 21 位,对齐到 2MB │ │
│ │ │ │
│ │ 337-339: 计算解压目标地址 │ │
│ │ movl BP_init_size(%rsi), %ebx │ │
│ │ # init_size = 内核运行时需要的内存大小 │ │
│ │ subl $ rva(_end), %ebx │ │
│ │ # 减去压缩内核大小 │ │
│ │ addq %rbp, %rbx │ │
│ │ # %rbx = 解压目标地址 │ │
│ │ │ │
│ │ 结果: %rbp = 内核最终运行地址 (2MB对齐) │ │
│ │ %rbx = 解压临时缓冲区地址 │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ PHASE 3: 设置栈和 GDT (行 342-374) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 342: leaq rva(boot_stack_end)(%rbx), %rsp │ │
│ │ # 设置栈指针到重定位后的栈顶 │ │
│ │ │ │
│ │ 366-368: 修正并加载 GDT │ │
│ │ leaq gdt64(%rip), %rax │ │
│ │ addq %rax, 2(%rax) # 修正 GDT 基址 │ │
│ │ lgdt (%rax) # 加载 GDT │ │
│ │ │ │
│ │ 371-374: 使用 lretq 重新加载 CS │ │
│ │ pushq $__KERNEL_CS │ │
│ │ leaq .Lon_kernel_cs(%rip), %rax │ │
│ │ pushq %rax │ │
│ │ lretq # 远返回,加载新 CS │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ PHASE 4: C 函数调用准备 (行 376-417) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ .Lon_kernel_cs: │ │
│ │ 382: movq %rsi, %r15 # 保存 boot_params 指针 │ │
│ │ # %r15 是 callee-saved,C函数调用不会破坏 │ │
│ │ │ │
│ │ 384: call load_stage1_idt # 加载早期 IDT │ │
│ │ │ │
│ │ 398: call sev_enable # AMD SEV 初始化 (如果需要) │ │
│ │ │ │
│ │ 401-404: 保留必要的 CR4 位 │ │
│ │ movq %cr4, %rax │ │
│ │ andl $(PAE|MCE|LA57), %eax │ │
│ │ movq %rax, %cr4 │ │
│ │ # 只保留 PAE, MCE, LA57,清除其他 │ │
│ │ │ │
│ │ 415-417: 配置 5级分页 (如果需要) │ │
│ │ movq %r15, %rdi # arg1: boot_params │ │
│ │ leaq rva(top_pgtable)(%rbx), %rsi # arg2: 页表 │ │
│ │ call configure_5level_paging │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ PHASE 5: 重定位压缩内核 (行 427-433) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # 将压缩内核移到缓冲区末尾,避免解压时覆盖 │ │
│ │ 427: leaq (_bss-8)(%rip), %rsi │ │
│ │ # 源地址: 压缩内核末尾 - 8 │ │
│ │ 428: leaq rva(_bss-8)(%rbx), %rdi │ │
│ │ # 目标地址: 重定位后的位置 │ │
│ │ 429: movl $(_bss - startup_32), %ecx │ │
│ │ # 大小: 从 startup_32 到 _bss │ │
│ │ 430: shrl $3, %ecx # 转换为 QWORD 数量 │ │
│ │ 431: std # 设置方向标志 (向下) │ │
│ │ 432: rep movsq # 从高地址往低地址复制 │ │
│ │ 433: cld # 清除方向标志 │ │
│ │ │ │
│ │ 为什么反向复制? │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ 原位置: [====压缩内核====][空闲空间] │ │ │
│ │ │ 新位置: [空闲][====压缩内核====] │ │ │
│ │ │ 如果正向复制,可能覆盖源数据 │ │ │
│ │ │ 反向复制确保安全 │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────┬────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ PHASE 6: 重新加载 GDT 并跳转 (行 440-449) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 440-443: 重新加载 GDT │ │
│ │ # GDT 可能在复制时被覆盖,需要重新指向新位置 │ │
│ │ leaq rva(gdt64)(%rbx), %rax │ │
│ │ leaq rva(gdt)(%rbx), %rdx │ │
│ │ movq %rdx, 2(%rax) │ │
│ │ lgdt (%rax) │ │
│ │ │ │
│ │ 448-449: 跳转到重定位后的代码 │ │
│ │ leaq rva(.Lrelocated)(%rbx), %rax │ │
│ │ jmp *%rax │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────┬────────────────────────────────────────┘
↓
.Lrelocated
↓
(extract_kernel)
真正内核 阶段
secondary_startup_64
从 secondary_startup_64 开始执行
SYM_CODE_START(secondary_startup_64)
UNWIND_HINT_END_OF_STACK
ANNOTATE_NOENDBR
......
1: cmpl %edx, %eax
je 1f
xor %edx, %edx
wrmsr /* Make changes effective */
1:
/* Setup cr0 */
movl $CR0_STATE, %eax
/* Make changes effective */
movq %rax, %cr0
/* zero EFLAGS after setting rsp */
pushq $0
popfq
/* Pass the boot_params pointer as first argument */
movq %r15, %rdi
.Ljump_to_C_code:
xorl %ebp, %ebp # clear frame pointer
ANNOTATE_RETPOLINE_SAFE
callq *initial_code(%rip)
ud2
SYM_CODE_END(secondary_startup_64)
流程图
═══════════════════════════════════════════════════════════════════
secondary_startup_64 详细执行流程
═══════════════════════════════════════════════════════════════════
┌───────────────────────────────────────────────────────────────┐
│ 入口状态: │
│ - CPU: 64位长模式 (CS.L=1, CS.D=0) │
│ - 页表: 已加载 (BSP 用 early_top_pgt, AP 用 trampoline_pgd) │
│ - %rsi: boot_params 地址 (BSP) 或 未定义 (AP) │
└───────────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────┐
│ 阶段 1: CPU 验证 (行 152-163) │
├───────────────────────────────────────────────────────────────┤
│ 153: call verify_cpu │
│ ↓ │
│ 检查 CPU 是否支持 64位 long mode │
│ 验证必要的 CPU 特性 (如 NX 位) │
│ │
│ 164: SYM_INNER_LABEL(secondary_startup_64_no_verify) │
│ ↓ │
│ SEV-ES 客户机的特殊入口点(跳过 verify_cpu) │
└───────────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────┐
│ 阶段 2: 清除 boot_params 指针 (行 168-169) │
├───────────────────────────────────────────────────────────────┤
│ 169: xorl %r15d, %r15d │
│ ↓ │
│ 清除 %r15 (在 startup_64 中保存了 boot_params) │
│ │
│ 原因: │
│ - BSP 在 startup_64 中已经使用过 boot_params │
│ - AP 核心不需要 boot_params │
│ - 清零防止误用 │
└───────────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────┐
│ 阶段 3: 计算并加载内核页表 (行 171-186) │
├───────────────────────────────────────────────────────────────┤
│ 171-173: 计算 init_top_pgt 的物理地址 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ movq phys_base(%rip), %rax │ │
│ │ # phys_base = 内核实际加载的物理基址 │ │
│ │ │ │
│ │ addq $(init_top_pgt - __START_KERNEL_map), %rax │ │
│ │ # __START_KERNEL_map = 0xFFFFFFFF80000000 (内核虚拟基址)│ │
│ │ # init_top_pgt - __START_KERNEL_map = 相对偏移 │ │
│ │ # %rax = phys_base + 偏移 = init_top_pgt 物理地址 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 179-181: 添加 SME 加密掩码 (如果启用了 AMD 内存加密) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ #ifdef CONFIG_AMD_MEM_ENCRYPT │ │
│ │ addq sme_me_mask(%rip), %rax │ │
│ │ #endif │ │
│ │ # sme_me_mask = C-bit 位置标记 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 186: movq %rax, %cr3 │
│ ↓ │
│ 切换到内核的最终页表 init_top_pgt │
│ │
│ 效果: │
│ - 卸载 trampoline_pgd (AP 使用的临时页表) │
│ - 卸载恒等映射 (identity mapping) │
│ - 使用内核的虚拟地址空间 │
└───────────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────┐
│ 阶段 4: 进入共同启动路径 (行 188) │
├───────────────────────────────────────────────────────────────┤
│ 188: SYM_INNER_LABEL(common_startup_64, SYM_L_LOCAL) │
│ ↓ │
│ 从这里开始,BSP 和 AP 执行相同的代码 │
│ (BSP 从 startup_64 跳转到这里,AP 从上面流程下来) │
└───────────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────┐
│ 阶段 5: 配置 CR4 寄存器 (行 192-223) │
├───────────────────────────────────────────────────────────────┤
│ 目的: 设置 CPU 特性控制位,刷新 TLB │
│ │
│ 201-213: 创建 CR4 保留掩码 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ movl $(X86_CR4_PAE | X86_CR4_LA57), %edx │ │
│ │ # PAE = Physical Address Extension │ │
│ │ # LA57 = 5级页表支持 │ │
│ │ │ │
│ │ #ifdef CONFIG_X86_MCE │ │
│ │ orl $X86_CR4_MCE, %edx │ │
│ │ # MCE = Machine Check Exception │ │
│ │ #endif │ │
│ │ │ │
│ │ movq %cr4, %rcx # 读取当前 CR4 │ │
│ │ andl %edx, %ecx # 保留必要的位 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 关键点: 暂时不设置 PGE (Page Global Enable) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 为什么? │ │
│ │ │ │
│ │ 从 SDM (Software Developer's Manual): │ │
│ │ "If CR4.PGE is changing from 1 to 0, │ │
│ │ there will be no global TLB entries after execution." │ │
│ │ │ │
│ │ 目的: 通过 PGE 0→1→0 的变化清除全局 TLB 条目 │ │
│ │ (特别是清除恒等映射的 TLB) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 216-217: 设置 PSE (Page Size Extension) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ btsl $X86_CR4_PSE_BIT, %ecx │ │
│ │ movq %rcx, %cr4 │ │
│ │ # PSE = 支持 4MB/2MB 大页 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 222-223: 重新启用 PGE │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ btsl $X86_CR4_PGE_BIT, %ecx │ │
│ │ movq %rcx, %cr4 │ │
│ │ # PGE = 全局页启用,避免频繁刷新内核页表 │ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────┐
│ 阶段 6: 确定 CPU 编号 (行 225-307) │
├───────────────────────────────────────────────────────────────┤
│ #ifdef CONFIG_SMP │
│ │
│ 234-242: 检查启动模式 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ movl smpboot_control(%rip), %ecx │ │
│ │ testl $STARTUP_READ_APICID, %ecx │ │
│ │ jnz .Lread_apicid │ │
│ │ │ │
│ │ 两种模式: │ │
│ │ 1. 单 CPU 启动: CPU# 直接在 smpboot_control 低 24 位 │ │
│ │ 2. 并行启动: 从 APIC 读取 APIC ID,然后查表 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 244-277: 读取 APIC ID (.Lread_apicid) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 方法 1: X2APIC MSR (性能更好) │ │
│ │ mov $MSR_IA32_APICBASE, %ecx │ │
│ │ rdmsr │ │
│ │ testl $X2APIC_ENABLE, %eax │ │
│ │ jnz .Lread_apicid_msr │ │
│ │ ↓ │ │
│ │ mov $APIC_X2APIC_ID_MSR, %ecx │ │
│ │ rdmsr # %eax = APIC ID │ │
│ │ │ │
│ │ 方法 2: MMIO 方式读取 (传统方法) │ │
│ │ movq apic_mmio_base(%rip), %rcx │ │
│ │ addq $APIC_ID, %rcx │ │
│ │ movl (%rcx), %eax # 内存映射读取 │ │
│ │ shr $24, %eax # 提取 APIC ID │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 278-292: APIC ID → CPU 编号映射 (.Llookup_AP) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ xorl %ecx, %ecx # %ecx = CPU 索引 │ │
│ │ leaq cpuid_to_apicid(%rip), %rbx │ │
│ │ # cpuid_to_apicid[] = 映射表: cpuid_to_apicid[CPU#] = │ │
│ │ APIC_ID │ │
│ │ │ │
│ │ .Lfind_cpunr: │ │
│ │ cmpl (%rbx,%rcx,4), %eax # 比较 APIC ID │ │
│ │ jz .Lsetup_cpu # 找到了,跳转 │ │
│ │ inc %ecx # 下一个 CPU │ │
│ │ cmpl $NR_CPUS, %ecx │ │
│ │ jb .Lfind_cpunr # 继续查找 │ │
│ │ │ │
│ │ # 如果找不到 APIC ID,释放锁并挂起 │ │
│ │ movq trampoline_lock(%rip), %rax │ │
│ │ movl $0, (%rax) # 释放锁 │ │
│ │ cli; hlt; jmp 1b # 挂起 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 结果: %ecx = CPU 编号 (0, 1, 2, ...) │
│ #endif │
└───────────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────┐
│ 阶段 7: 设置 per-CPU 数据和栈 (行 303-326) │
├───────────────────────────────────────────────────────────────┤
│ .Lsetup_cpu: │
│ │
│ 304: 获取 per-CPU 偏移量 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ #ifdef CONFIG_SMP │ │
│ │ movq __per_cpu_offset(,%rcx,8), %rdx │ │
│ │ # __per_cpu_offset[] 数组存储每个 CPU 的数据区偏移 │ │
│ │ # %rdx = per-CPU 数据区的基址 │ │
│ │ #else │ │
│ │ xorl %edx, %edx # UP 系统没有偏移 │ │
│ │ #endif │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 315-316: 设置栈指针 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ movq pcpu_hot + X86_current_task(%rdx), %rax │ │
│ │ # pcpu_hot.current_task = 当前任务 (task_struct) │ │
│ │ # 对于 BSP: init_task │ │
│ │ # 对于 AP: 预先分配的 idle 任务 │ │
│ │ │ │
│ │ movq TASK_threadsp(%rax), %rsp │ │
│ │ # %rsp = task->thread.sp (任务的内核栈顶) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 关键点: 栈切换 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 为什么需要切换栈? │ │
│ │ │ │
│ │ - BSP: 之前使用 early stack (临时栈) │ │
│ │ - AP: 之前使用 trampoline 中的栈 (已被 CR3 切换卸载) │ │
│ │ │ │
│ │ 现在: 每个 CPU 都使用自己的 per-CPU 内核栈 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 322-325: 释放 trampoline 锁 (仅 AP) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ movq trampoline_lock(%rip), %rax │ │
│ │ testq %rax, %rax # BSP 的锁指针为 NULL │ │
│ │ jz .Lsetup_gdt │ │
│ │ movl $0, (%rax) # AP 释放锁,允许下一个启动│ │
│ │ │ │
│ │ 作用: 串行化 AP 启动,一次只有一个 AP 执行这段代码 │ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────┐
│ 阶段 8: 加载 per-CPU GDT (行 327-353) │
├───────────────────────────────────────────────────────────────┤
│ .Lsetup_gdt: │
│ │
│ 334-339: 加载新的 GDT │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ subq $16, %rsp # 在栈上分配 GDT 描述符空间│ │
│ │ movw $(GDT_SIZE-1), (%rsp) # GDT 界限 │ │
│ │ leaq gdt_page(%rdx), %rax # GDT 基址 (per-CPU) │ │
│ │ movq %rax, 2(%rsp) # 存储基址 │ │
│ │ lgdt (%rsp) # 加载 GDT │ │
│ │ addq $16, %rsp # 恢复栈 │ │
│ │ │ │
│ │ 为什么需要新的 GDT? │ │
│ │ - 之前使用的是临时/共享的 GDT │ │
│ │ - 现在每个 CPU 需要自己的 GDT (在 per-CPU 数据区) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 342-353: 设置段寄存器 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ xorl %eax, %eax │ │
│ │ movl %eax, %ds # DS = 0 (64位模式平坦) │ │
│ │ movl %eax, %ss # SS = 0 │ │
│ │ movl %eax, %es # ES = 0 │ │
│ │ movl %eax, %fs # FS = 0 │ │
│ │ movl %eax, %gs # GS = 0 (稍后设置基址) │ │
│ │ │ │
│ │ 64位长模式下段寄存器的作用: │ │
│ │ - 选择子值不重要 (平坦内存模型) │ │
│ │ - 但 FS/GS 的基址仍然有用 (通过 MSR 设置) │ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────┐
│ 阶段 9: 设置 GS 基址 (行 355-368) │
├───────────────────────────────────────────────────────────────┤
│ 362-368: 设置 MSR_GS_BASE │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ movl $MSR_GS_BASE, %ecx │ │
│ │ │ │
│ │ #ifdef CONFIG_SMP │ │
│ │ # SMP: GS 指向 per-CPU 的 fixed_percpu_data │ │
│ │ # (已经在 %rdx 中 = per-CPU 基址) │ │
│ │ #else │ │
│ │ # UP: GS 指向 BSS 中的 init_per_cpu 区域 │ │
│ │ leaq INIT_PER_CPU_VAR(fixed_percpu_data)(%rip), %rdx │ │
│ │ #endif │ │
│ │ │ │
│ │ movl %edx, %eax # MSR 低 32 位 │ │
│ │ shrq $32, %rdx # MSR 高 32 位 │ │
│ │ wrmsr # 写入 MSR_GS_BASE │ │
│ │ │ │
│ │ 结果: GS 段的基址 = fixed_percpu_data │ │
│ │ %gs:0x28 = stack canary (栈溢出保护) │ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────┐
│ 阶段 10: 设置 IDT (行 370-371) │
├───────────────────────────────────────────────────────────────┤
│ 371: call early_setup_idt │
│ ↓ │
│ 加载早期的 IDT (bringup_idt_table) │
│ 包含基本的异常处理程序 │
│ │
│ 为什么需要早期 IDT? │
│ - 最终的 idt_table 还未完全初始化 │
│ - 需要基本的异常处理能力 (如页错误) │
│ - 避免使用 tracing/KASAN 检测的代码 │
└───────────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────┐
│ 阶段 11: 配置 EFER 和 CR0 (行 373-401) │
├───────────────────────────────────────────────────────────────┤
│ 374-376: 检查 NX 支持 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ movl $0x80000001, %eax # CPUID 扩展功能查询 │ │
│ │ cpuid │ │
│ │ movl %edx, %edi # 保存特性位 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 379-396: 设置 EFER (Extended Feature Enable Register) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ movl $MSR_EFER, %ecx │ │
│ │ rdmsr # 读取当前 EFER │ │
│ │ movl %eax, %edx # 保存原值 (TDX 需要) │ │
│ │ │ │
│ │ btsl $_EFER_SCE, %eax # SCE = System Call Enable│ │
│ │ # 启用 SYSCALL/SYSRET │ │
│ │ │ │
│ │ btl $20, %edi # 检查 bit 20 (NX) │ │
│ │ jnc 1f # 不支持则跳过 │ │
│ │ btsl $_EFER_NX, %eax # NX = No Execute │ │
│ │ btsq $_PAGE_BIT_NX, early_pmd_flags(%rip) │ │
│ │ # 同时在页表标志中启用 NX │ │
│ │ │ │
│ │ 1: cmpl %edx, %eax # 检查是否有变化 │ │
│ │ je 1f # 没变化则跳过写入 │ │
│ │ xor %edx, %edx │ │
│ │ wrmsr # 写入 EFER │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 398-401: 设置 CR0 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ movl $CR0_STATE, %eax │ │
│ │ # CR0_STATE = PE|MP|ET|NE|WP|AM|PG │ │
│ │ movq %rax, %cr0 │ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
↓
┌───────────────────────────────────────────────────────────────┐
│ 阶段 12: 准备跳转到 C 代码 (行 403-414) │
├───────────────────────────────────────────────────────────────┤
│ 403-405: 清零 EFLAGS │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ pushq $0 │ │
│ │ popfq # EFLAGS = 0 │ │
│ │ # 清除所有标志位,进入干净状态 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 407-408: 传递参数 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ movq %r15, %rdi # 第一个参数 │ │
│ │ # BSP: %r15 = boot_params 地址 │ │
│ │ # AP: %r15 = 0 (已清零) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 410-413: 跳转到 C 代码 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ .Ljump_to_C_code: │ │
│ │ xorl %ebp, %ebp # 清除帧指针 (栈回溯结束) │ │
│ │ callq *initial_code(%rip) │ │
│ │ │ │
│ │ initial_code 变量的值: │ │
│ │ - BSP: x86_64_start_kernel │ │
│ │ - AP: start_secondary │ │
│ │ │ │
│ │ ud2 # 不应该返回,如果返回则挂起│ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
↓
[进入 C 代码]
为什么被加载到0x9d000?(realmode加载探索)
回到最初我们的问题.那么就得探究一下realmode trampoline在内存中的加载
流程图
启动阶段流程:
═══════════════════════════════════════════════════════════════
[编译时]
│
├─→ realmode.bin (编译实模式代码)
│ - trampoline_64.S
│ - 其他实模式代码
│
├─→ realmode.relocs (重定位信息)
│
└─→ rmpiggy.S (.incbin包含上述文件)
└─→ real_mode_blob (数据在内核镜像中)
[内核启动 - setup_arch() 之前]
│
▼
┌──────────────────────────────────────┐
│ reserve_real_mode() │
│ (init.c:49) │
│ ┌────────────────────────────────┐ │
│ │ 1. 计算需要的大小 │ │
│ │ real_mode_size_needed() │ │
│ │ = PAGE_ALIGN(blob_end - │ │
│ │ blob_start) │ │
│ │ │ │
│ │ 2. 分配低内存(<1MB) │ │
│ │ memblock_phys_alloc_range() │ │
│ │ 范围: 0 ~ 1MB │ │
│ │ 对齐: PAGE_SIZE │ │
│ │ │ │
│ │ 3. 设置real_mode_header │ │
│ │ set_real_mode_mem(mem) │ │
│ │ real_mode_header = │ │
│ │ __va(物理地址) │ │
│ │ │ │
│ │ 4. 保留整个1MB内存 │ │
│ │ memblock_reserve(0, 1M) │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘
│
▼
[内核启动 - do_pre_smp_initcalls()]
│
▼
┌──────────────────────────────────────┐
│ init_real_mode() │
│ (init.c:221) │
│ ┌────────────────────────────────┐ │
│ │ setup_real_mode() │ │
│ │ set_real_mode_permissions() │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘
│
▼
[详细的setup流程见下]
生命周期
┌──────────────────────────────────────────────────────────────┐
│ REAL_MODE 生命周期 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 编译时: │
│ ├─ 编译 trampoline_64.S 等实模式代码 │
│ ├─ 生成 realmode.bin │
│ └─ 通过 rmpiggy.S 打包到内核镜像 │
│ │
│ 启动早期 (reserve_real_mode): │
│ ├─ 在低内存(<1MB)保留空间 │
│ └─ 设置 real_mode_header 指针 │
│ │
│ 初始化时 (setup_real_mode): │
│ ├─ 复制代码到低内存 │
│ ├─ 执行重定位修正 │
│ ├─ 配置 trampoline_header │
│ ├─ 设置 trampoline_pgd │
│ └─ 设置内存权限 │
│ │
│ 运行时: │
│ ├─ SMP启动: 每个AP通过trampoline启动 │
│ ├─ CPU热插拔: 新CPU上线时使用 │
│ ├─ ACPI S3: 从睡眠唤醒时使用 │
│ └─ 系统重启: 某些重启路径使用 │
│ │
│ 特点: │
│ ├─ 物理地址: ~0x90000 (低内存) │
│ ├─ 虚拟地址: 直接映射区 (0xffff8880_00090000) │
│ ├─ 大小: ~4KB (1-2个页面) │
│ ├─ 权限: 代码可执行,数据可读写 │
│ └─ 生命周期: 从初始化到系统关闭一直存在 │
│ │
└──────────────────────────────────────────────────────────────┘
调用链
// 在启动过程中的调用顺序:
start_kernel()
→ setup_arch()
→ reserve_real_mode() // [给 realmode 代码留内存空间]
→ ... 其他初始化 ...
→ rest_init()
→ kernel_init()
→ kernel_init_freeable()
→ do_basic_setup()
→ do_initcalls()
→ init_real_mode() // 通过early_initcall注册
内存演化图
═══════════════════════════════════════════════════════════════════
Linux内核启动的完整内存使用流程
═══════════════════════════════════════════════════════════════════
[时刻0: Bootloader工作阶段]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Bootloader (如GRUB) 读取内核镜像文件 (bzImage):
bzImage结构:
┌────────────────────────────────────┐
│ Setup部分 (setup.bin) │ ← 从文件开始
│ - header.S, main.c, pm.c编译的 │
│ - 16位实模式代码 │
│ - 大小: ~32KB │
├────────────────────────────────────┤
│ 压缩内核部分 (vmlinux.bin.gz) │
│ - head_64.S + 压缩的vmlinux │
│ - 32/64位代码 │
│ - 大小: 几MB │
└────────────────────────────────────┘
Bootloader执行操作:
│
├─→ 1. 读取setup部分,加载到内存
│ 目标: 0x90000 (传统位置)
│
├─→ 2. 读取压缩内核部分,加载到内存
│ 目标: 0x100000 (1MB)
│ ↑
│ └─ 这个地址存储在header.S的code32_start字段
│
└─→ 3. 跳转到0x90000开始执行
(跳转到Setup代码的_start)
[时刻1: Setup代码运行 - 第一次使用0x90000]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
物理内存布局:
0x00000000 ┌─────────────────────────┐
│ BIOS数据区 │
0x00007C00 ├─────────────────────────┤
│ Bootloader残留 │
│ │
0x00090000 ├─────────────────────────┤ ← 当前执行位置!
│ ★板块1: Setup代码★ │ ← Bootloader放这里的
│ │
│ 包含: │
│ • header.S (_start) │
│ • main.c (main) │
│ • pm.c │
│ • boot_params │
│ • 命令行 │
│ • 堆/栈 │
│ │
0x00098000 │ (setup代码约32KB) │
├─────────────────────────┤
0x000A0000 │ VGA显存 │
├─────────────────────────┤
0x00100000 ├─────────────────────────┤ ← code32_start指向这里
│ ★板块2: 压缩内核★ │ ← Bootloader放这里的
│ │
│ 包含: │
│ • startup_32 │
│ • startup_64 │
│ • 压缩的vmlinux数据 │
│ │
│ (几MB大小) │
│ │
├─────────────────────────┤
│ 更高内存... │
Setup代码执行流程:
│
├─→ header.S:_start
│ • 设置栈、段寄存器
│ • 清零BSS
│ • calll main
│
├─→ main.c:main()
│ • copy_boot_params() ← 复制header到boot_params
│ • detect_memory() ← 检测内存
│ • set_video() ← 设置显示
│ • go_to_protected_mode()
│
├─→ pm.c:go_to_protected_mode()
│ • enable_a20()
│ • setup_idt()
│ • setup_gdt()
│ • protected_mode_jump(boot_params.hdr.code32_start, ...)
│ ↑
│ └─ 这是0x100000!
│
└─→ pmjump.S:protected_mode_jump()
• 启用保护模式 (CR0.PE=1)
• jmpl *%eax ← 跳转到0x100000
← 跳转到★板块2: 压缩内核★
[时刻2: 压缩内核运行 - 使用0x100000]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
物理内存布局:
0x00090000 ┌─────────────────────────┐
│ Setup代码 (不再执行) │ ← 保留boot_params
│ boot_params仍然在这里 │
├─────────────────────────┤
0x000A0000 │ VGA显存 │
├─────────────────────────┤
0x00100000 ├─────────────────────────┤ ← 当前执行位置!
│ ★板块2: 压缩内核★ │ ← 正在这里运行
│ │
│ 执行: │
│ • startup_32 │
│ - 建立页表 │
│ - 进入长模式 │
│ • startup_64 │
│ - 重定位代码 │
│ - extract_kernel() │
│ (解压vmlinux) │
│ │
├─────────────────────────┤
0x01000000 ├─────────────────────────┤ ← LOAD_PHYSICAL_ADDR
│ 解压目标区域 │ ← extract_kernel()解压到这里
│ (真正的内核) │
│ │
压缩内核执行流程:
│
├─→ head_64.S:startup_32 (入口点)
│ • 建立早期页表
│ • 启用PAE
│ • 启用长模式
│ • lret → startup_64
│
├─→ head_64.S:startup_64
│ • configure_5level_paging()
│ • 重定位压缩内核
│ • 清空BSS
│ • extract_kernel() ← 解压到0x1000000
│ • jmp *%rax ← 跳转到解压后的内核
│
└─→ 跳转到0x1000000处的真正内核
[时刻3: 真正内核运行 - 使用0x1000000]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
物理内存布局:
0x00090000 ┌─────────────────────────┐
│ Setup代码 (已失效) │ ← 这块内存会被重用!
│ │
├─────────────────────────┤
0x000A0000 │ VGA显存 │
├─────────────────────────┤
0x00100000 ├─────────────────────────┤
│ 压缩内核 (不再使用) │ ← 可能被回收
│ │
├─────────────────────────┤
0x01000000 ├─────────────────────────┤ ← 当前执行位置!
│ ★真正的解压后内核★ │
│ │
│ arch/x86/kernel/head_64.S│
│ • startup_64 │
│ • x86_64_start_kernel() │
│ • start_kernel() │
│ │
真正内核执行流程:
│
├─→ kernel/head_64.S:startup_64
│ • 设置内核GDT/IDT
│ • 设置内核页表
│ • __startup_64()
│ • x86_64_start_kernel()
│
├─→ kernel/head64.c:x86_64_start_kernel()
│ • 复制boot_params
│ • 早期初始化
│ • start_kernel()
│
└─→ init/main.c:start_kernel()
│
├─→ setup_arch() ← 重要!这里设置realmode trampoline
│ │
│ ├─→ reserve_real_mode()
│ │ ↓
│ │ 在0x90000附近保留内存
│ │ ↓
│ │ 这是为★板块3★准备的!
│ │
│ └─→ init_real_mode()
│ └─→ setup_real_mode()
│ ↓
│ 将realmode trampoline复制到0x90000
│ ↓
│ 这是★板块3: Realmode Trampoline★
│
└─→ 继续内核初始化...
[时刻4: 内核运行时 - 第二次使用0x90000]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
物理内存布局:
0x00090000 ┌─────────────────────────┐
│ ★板块3: Realmode │ ← 内核重新设置的!
│ Trampoline★ │ ← setup_real_mode()复制的
│ │
│ 包含: │
│ • trampoline_64.S │
│ • trampoline_header │
│ • trampoline_pgd │
│ │
│ 用途: │
│ • SMP启动AP处理器 │
│ • ACPI S3唤醒 │
│ • 系统重启 │
│ │
├─────────────────────────┤
0x000A0000 │ VGA显存 │
├─────────────────────────┤
│ │
0x01000000 ├─────────────────────────┤
│ 运行中的内核 │ ← 当前执行位置
│ │
│ │
├─────────────────────────┤
│ 内核数据结构 │
│ 页表 │
│ 模块 │
│ ... │
何时使用板块3?
═══════════════════════════════════════════════════════════════
1. SMP启动时: 每个AP核心从0x90000开始执行
BSP通过SIPI中断通知AP: "去0x90000执行代码"
AP: 16位实模式 → 32位 → 64位 → secondary_startup_64
2. ACPI S3唤醒: 从睡眠恢复时重新初始化
3. 某些系统重启路径
预留内存空间(reserve_real_mode)
在 setup_arch(command_line) 阶段为我们的切换程序留内存运行空间为后续启动过程中的代码预留好空间
在系统内存低端(1MB以下)为实模式蹦床代码预留内存区域,确保多处理器启动时APs有可用的实模式执行环境。
void __init setup_arch(char **cmdline_p)
{
...
/*
* 为实模式跳转程序(real mode trampoline)寻找空闲内存并放置。
* 若 1MB 以下空间无足够空闲内存,在启用 EFI 的系统上,
* 会在 efi_free_boot_services() 阶段再次尝试回收内存以分配给实模式跳转程序。
*
* 无条件保留物理内存的前 1MB —— 原因是已知 BIOS 会破坏低地址内存,
* 而这几百 KB 的空间不值得通过复杂检测来判断哪些内存会被篡改。
* Windows 也因类似原因采用了相同的策略。
*
* 此外,在搭载 SandyBridge 核显的设备或启用 crashkernel(崩溃内核)的配置中,
* 前 1MB 内存本就会被自动保留。
*
* 注意:支持 TDX(Trust Domain Extensions)的宿主内核也要求保留前 1MB 内存。
*/
x86_platform.realmode_reserve();
...
}
/*
reserve_real_mode():在内存低端(<1MB)保留实模式代码运行所需的内存区域
为AP(Application Processors,即非BSP处理器)启动代码提供运行环境
*/
void __init reserve_real_mode(void)
{
phys_addr_t mem;
size_t size = real_mode_size_needed();
if (!size)
return;
WARN_ON(slab_is_available());
/* Has to be under 1M so we can execute real-mode AP code. */
mem = memblock_phys_alloc_range(size, PAGE_SIZE, 0, 1<<20);
if (!mem)
pr_info("No sub-1M memory is available for the trampoline\n");
else
set_real_mode_mem(mem);
/*
* Unconditionally reserve the entire first 1M, see comment in
* setup_arch().
*/
memblock_reserve(0, SZ_1M);
}
寻找空闲内存过程
这个文章从历史角度也分析了 realmode code的内存加载位置:https://linuxkernel.org.cn/doc/html/latest/arch/x86/boot.html
// arch/x86/realmode/init.c:49
void __init reserve_real_mode(void)
{
phys_addr_t mem;
size_t size = real_mode_size_needed(); // 通常4KB左右
// 关键:在 0 到 1MB 范围内分配
mem = memblock_phys_alloc_range(size, PAGE_SIZE, 0, 1<<20);
// ↑ ↑
// 起始 结束(1MB)
}
memblock分配器的行为:
- 从高地址向低地址搜索可用内存
- 寻找满足大小和对齐要求的第一个空闲块
- 避开已保留的区域
那么我们分析一下前面出现的内存占用情况就能很快估算出来内存大概率应该存在的地址
启动时间线:
═══════════════════════════════════════════════════════════════
[1] setup_arch() 早期
↓
reserve_bios_regions() // 先保留BIOS相关区域
↓
保留 0xA0000-0xFFFFF (VGA + BIOS ROM)
保留 EBDA区域 (如果存在)
↓
[2] setup_arch() 中
↓
reserve_real_mode()
↓
在剩余空间中分配 (~4KB)
↓
memblock从高到低搜索:
┌─────────────────────────────────────────┐
│ 0xA0000-0xFFFFF: 已保留(VGA/BIOS) │
│ ▼ 向下搜索 │
│ 0x9F000-0x9FFFF: 可能被EBDA占用 │
│ ▼ 继续向下 │
│ 0x90000-0x9EFFF: ★找到了!可用空间★ │
│ - 4KB大小足够 │
│ - PAGE_SIZE对齐 │
│ - 在<1MB范围内 │
│ - 实模式可访问 │
│ → 分配在这里! │
└─────────────────────────────────────────┘
Set_real_mode_mem
static inline void set_real_mode_mem(phys_addr_t mem)
{
real_mode_header = (struct real_mode_header *) __va(mem);
}
do_init_real_mode
Initcall 机制可以详细看知识库中的 initcall 机制板块,这里就代表着 do_init_real_mode 会在 kernel_init_freeable 的时候通过 do_pre_smp_initcalls 自动调用 (do_fork 启动一个 进程 执行 kernel_init 函数. PID 为 1 的进程. —> kernel_init_freeable函数 + run_init_process)
static int __init do_init_real_mode(void)
{
x86_platform.realmode_init();
return 0;
}
early_initcall(do_init_real_mode);
调用链
do_init_real_mode()
init_real_mode()
setup_real_mode() [初始化 realmode]
setup_real_mode
static void __init setup_real_mode(void)
{
...
size_t size = PAGE_ALIGN(real_mode_blob_end - real_mode_blob);
#ifdef CONFIG_X86_64
u64 *trampoline_pgd;
u64 efer;
int i;
#endif
base = (unsigned char *)real_mode_header;
/*
* If SME is active, the trampoline area will need to be in
* decrypted memory in order to bring up other processors
* successfully. This is not needed for SEV.
*/
if (cc_platform_has(CC_ATTR_HOST_MEM_ENCRYPT))
set_memory_decrypted((unsigned long)base, size >> PAGE_SHIFT);
// 二进制代码复制到对应内存区域
memcpy(base, real_mode_blob, size);
...
}
-
流程图
-
┌─────────────────────────────────────────────────────┐
│ setup_real_mode() 执行流程 │
│ (init.c:92-181) │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 步骤1: 获取目标地址 │
│ ┌───────────────────────────────────────────────┐ │
│ │ base = (unsigned char *)real_mode_header │ │
│ │ │ │
│ │ 虚拟地址: 例如 0xffff888000090000 │ │
│ │ 物理地址: __pa(base) (例如 0x90000) │ │
│ │ │ │
│ │ size = PAGE_ALIGN(real_mode_blob_end - │ │
│ │ real_mode_blob) │ │
│ │ 通常为 1~2个页面 │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 步骤2: 复制代码 (行117) │
│ ┌───────────────────────────────────────────────┐ │
│ │ memcpy(base, real_mode_blob, size) │ │
│ │ │ │
│ │ 源: real_mode_blob │ │
│ │ - 位于内核镜像的.init.data段 │ │
│ │ - 虚拟地址: __init区域 │ │
│ │ - 包含编译好的realmode.bin │ │
│ │ │ │
│ │ 目标: base │ │
│ │ - 位于低内存(<1MB) │ │
│ │ - 物理地址: 通常0x90000附近 │ │
│ │ - AP处理器启动时能访问 │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 步骤3: 计算重定位基地址 (行119-120) │
│ ┌───────────────────────────────────────────────┐ │
│ │ phys_base = __pa(base) │ │
│ │ real_mode_seg = phys_base >> 4 │ │
│ │ │ │
│ │ 举例: │ │
│ │ phys_base = 0x90000 │ │
│ │ real_mode_seg = 0x9000 │ │
│ │ (实模式段地址格式: segment:offset) │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 步骤4: 16位段重定位 (行124-129) │
│ ┌───────────────────────────────────────────────┐ │
│ │ rel = (u32 *) real_mode_relocs │ │
│ │ count = *rel++ // 读取重定位项数量 │ │
│ │ │ │
│ │ while (count--) { │ │
│ │ u16 *seg = (u16 *)(base + *rel++) │ │
│ │ *seg = real_mode_seg // 修正段地址 │ │
│ │ } │ │
│ │ │ │
│ │ 作用: 修正实模式代码中的段地址引用 │ │
│ │ 例如: mov ax, SEGMENT_PLACEHOLDER │ │
│ │ ↓ 修正为 │ │
│ │ mov ax, 0x9000 │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 步骤5: 32位线性重定位 (行131-136) │
│ ┌───────────────────────────────────────────────┐ │
│ │ count = *rel++ │ │
│ │ │ │
│ │ while (count--) { │ │
│ │ u32 *ptr = (u32 *)(base + *rel++) │ │
│ │ *ptr += phys_base // 加上物理基地址 │ │
│ │ } │ │
│ │ │ │
│ │ 作用: 修正32位保护模式中的线性地址 │ │
│ │ 例如: ljmpl $__KERNEL32_CS, $0x0 │ │
│ │ ↓ 修正为 │ │
│ │ ljmpl $__KERNEL32_CS, $0x90100 │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 步骤6: 获取trampoline_header (行139-140) │
│ ┌───────────────────────────────────────────────┐ │
│ │ trampoline_header = │ │
│ │ (struct trampoline_header *) │ │
│ │ __va(real_mode_header->trampoline_header) │ │
│ │ │ │
│ │ real_mode_header->trampoline_header: │ │
│ │ - 是一个偏移量(相对于base) │ │
│ │ - 在header.S中定义为pa_trampoline_header │ │
│ │ │ │
│ │ 最终地址 = base + 偏移量 │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 步骤7: 配置trampoline_header (x86_64) │
│ (行151-162) │
│ ┌───────────────────────────────────────────────┐ │
│ │ struct trampoline_header { │ │
│ │ u64 start; // 启动入口点 │ │
│ │ u64 efer; // EFER寄存器值 │ │
│ │ u32 cr4; // CR4特性位 │ │
│ │ u32 flags; // 标志(SME等) │ │
│ │ u32 lock; // 自旋锁 │ │
│ │ }; │ │
│ │ │ │
│ │ 初始化: │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ rdmsrl(MSR_EFER, efer) │ │ │
│ │ │ th->efer = efer & ~EFER_LMA │ │ │
│ │ │ // 清除LMA位,AP启动时还在实模式 │ │ │
│ │ │ │ │ │
│ │ │ th->start = secondary_startup_64 │ │ │
│ │ │ // 64位AP入口点 │ │ │
│ │ │ │ │ │
│ │ │ th->cr4 = mmu_cr4_features │ │ │
│ │ │ // 当前CPU的CR4特性 │ │ │
│ │ │ │ │ │
│ │ │ th->flags = 0 │ │ │
│ │ │ th->lock = 0 │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 步骤8: 设置trampoline页表 (行164-177) │
│ ┌───────────────────────────────────────────────┐ │
│ │ trampoline_pgd = (u64 *) │ │
│ │ __va(real_mode_header->trampoline_pgd) │ │
│ │ │ │
│ │ // 恒等映射低内存 │ │
│ │ trampoline_pgd[0] = trampoline_pgd_entry.pgd │ │
│ │ ↑ │ │
│ │ 这使得虚拟地址 == 物理地址 (对于低内存) │ │
│ │ AP启动时需要这个映射 │ │
│ │ │ │
│ │ // 复制内核映射 │ │
│ │ for (i = pgd_index(__PAGE_OFFSET); │ │
│ │ i < PTRS_PER_PGD; i++) │ │
│ │ trampoline_pgd[i] = init_top_pgt[i].pgd │ │
│ │ ↑ │ │
│ │ 复制内核空间的所有映射 │ │
│ │ 这样AP可以访问内核代码和数据 │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
trampoline
作用
┌────────────────────────────────────────────────────────┐
│ trampoline 的三大作用: │
├────────────────────────────────────────────────────────┤
│ │
│ 1. SMP多核启动 (主要用途) │
│ ┌────────────────────────────────────────────┐ │
│ │ • BSP (Bootstrap Processor) 已启动 │ │
│ │ • 需要启动其他AP (Application Processors) │ │
│ │ • AP启动时处于16位实模式 │ │
│ │ • 需要过渡: 16位实模式 → 32位 → 64位长模式 │ │
│ │ • trampoline提供这个过渡代码 │ │
│ └────────────────────────────────────────────┘ │
│ │
│ 2. ACPI S3睡眠唤醒 │
│ ┌────────────────────────────────────────────┐ │
│ │ • 系统从S3睡眠状态唤醒 │ │
│ │ • CPU从实模式重新开始 │ │
│ │ • 使用wakeup_start代码段 │ │
│ └────────────────────────────────────────────┘ │
│ │
│ 3. APM/BIOS重启 │
│ ┌────────────────────────────────────────────┐ │
│ │ • 某些重启方式需要进入实模式 │ │
│ │ • 使用machine_real_restart_asm │ │
│ └────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────┘
生命周期
完整生命周期流程:
═══════════════════════════════════════════════════════════════
[阶段1: 编译时]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
时间: 内核编译时
位置: arch/x86/realmode/rm/
┌─────────────────────────────────────────┐
│ 1. 编译实模式代码 │
│ - trampoline_64.S │
│ - header.S │
│ - 生成 realmode.bin │
│ - 生成 realmode.relocs │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 2. 打包到内核镜像 │
│ - rmpiggy.S 使用 .incbin │
│ - 变成 real_mode_blob[] 数组 │
│ - 位于内核的 .init.data 段 │
└─────────────────────────────────────────┘
[阶段2: 内核启动早期 - BSP引导]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
时间: setup_arch() 中
调用: reserve_real_mode()
┌─────────────────────────────────────────┐
│ 1. 保留低内存 │
│ memblock_phys_alloc_range() │
│ ↓ │
│ 分配范围: 0 ~ 1MB │
│ 大小: ~4KB │
│ 对齐: PAGE_SIZE │
│ ↓ │
│ 物理地址: 例如 0x90000 │
│ ↓ │
│ 设置: real_mode_header = __va(mem) │
└─────────────────────────────────────────┘
│
│ 此时内存已保留,但还未复制代码
│
[阶段3: 初始化时 - 复制和配置]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
时间: do_pre_smp_initcalls()
调用: init_real_mode() → setup_real_mode()
┌─────────────────────────────────────────┐
│ 1. 复制代码到低内存 │
│ memcpy(base, real_mode_blob, size) │
│ ↓ │
│ 源: real_mode_blob (内核镜像) │
│ 目标: 0x90000 (物理内存) │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 2. 重定位修正 │
│ - 修正16位段地址 │
│ - 修正32位线性地址 │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 3. 配置trampoline_header │
│ - start = secondary_startup_64 │
│ - efer = 当前EFER & ~EFER_LMA │
│ - cr4 = mmu_cr4_features │
│ - lock = 0 │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 4. 设置trampoline_pgd页表 │
│ - [0] = 恒等映射 (低内存) │
│ - [高位] = 内核映射 │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 5. 设置内存权限 │
│ set_real_mode_permissions() │
│ - 代码段: 可执行 │
│ - 数据段: 可读写 │
│ - 只读段: 只读 │
└─────────────────────────────────────────┘
│
│ 此时trampoline已完全准备好
│
[阶段4: SMP启动 - 每个AP启动时]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
时间: do_boot_cpu() 中
每个AP启动时都会执行
┌─────────────────────────────────────────┐
│ BSP准备启动AP │
│ ┌───────────────────────────────────┐ │
│ │ 1. 设置initial_stack (per-CPU栈) │ │
│ │ 2. 设置initial_code (入口点) │ │
│ │ 3. 发送SIPI (启动处理器间中断) │ │
│ │ 目标地址: trampoline物理地址 │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ AP开始执行 (16位实模式) │
│ ┌───────────────────────────────────┐ │
│ │ trampoline_start: (trampoline_64.S)│ │
│ │ │ │
│ │ [16位实模式] │ │
│ │ 1. cli / wbinvd │ │
│ │ 2. 设置段寄存器 CS=DS=ES=SS │ │
│ │ 3. 获取自旋锁 (tr_lock) │ │
│ │ 4. 设置实模式栈 │ │
│ │ 5. 加载tr_gdt │ │
│ │ 6. 启用保护模式 (CR0.PE=1) │ │
│ │ 7. 跳转到32位代码 │ │
│ │ ljmpl $__KERNEL32_CS, $pa_... │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ AP继续执行 (32位保护模式) │
│ ┌───────────────────────────────────┐ │
│ │ startup_32: (trampoline_64.S) │ │
│ │ │ │
│ │ [32位保护模式] │ │
│ │ 1. 设置段寄存器 │ │
│ │ 2. 启用PAE (CR4.PAE=1) │ │
│ │ 3. 加载CR3 (trampoline_pgd) │ │
│ │ 4. 设置EFER (从trampoline_header)│ │
│ │ 5. 启用分页 (CR0.PG=1) │ │
│ │ → 进入兼容模式 │ │
│ │ 6. 跳转到64位代码 │ │
│ │ ljmpl $__KERNEL_CS, $pa_... │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ AP继续执行 (64位长模式) │
│ ┌───────────────────────────────────┐ │
│ │ startup_64: (trampoline_64.S) │ │
│ │ │ │
│ │ [64位长模式] │ │
│ │ 1. 从trampoline_header读取start │ │
│ │ 2. jmpq *tr_start │ │
│ │ → secondary_startup_64 │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ AP进入内核 (head_64.S) │
│ ┌───────────────────────────────────┐ │
│ │ secondary_startup_64: │ │
│ │ (arch/x86/kernel/head_64.S) │ │
│ │ │ │
│ │ 1. 加载内核GDT/IDT │ │
│ │ 2. 切换到内核页表 │ │
│ │ 3. 设置per-CPU数据 │ │
│ │ 4. 初始化栈 │ │
│ │ 5. 跳转到C代码 │ │
│ │ → start_secondary() │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
│
▼
[AP完全启动]
[阶段5: 运行时 - 持续存在]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
时间: 系统运行期间
状态: 保持在低内存中
┌─────────────────────────────────────────┐
│ trampoline代码持续存在于低内存 │
│ │
│ 可能的使用场景: │
│ • CPU热插拔 - 启动新CPU │
│ • ACPI S3唤醒 - 从睡眠恢复 │
│ • 系统重启 - 某些重启路径 │
└─────────────────────────────────────────┘
[阶段6: 关闭/重启时]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
时间: 系统关闭或重启
┌─────────────────────────────────────────┐
│ 某些重启路径会使用 │
│ machine_real_restart_asm │
│ │
│ ACPI睡眠会保留这块内存 │
└─────────────────────────────────────────┘
总结
那么这里总结一下,也就是说因为 realmode trampoline 在 Linux 内核启动后物理位置固定在 0x90000,然后trampoline_header->start 的位置相对于 realmode trampoline的位置也是固定的,因为setup_real_mode将secondary_startup_64存放在了trampoline_header->start,所以一般情况下 0x9d000 就固定存放着secondary_startup_64指针。