)
文档教程操作系统【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/gh_mirrors/li/linux-insides-zh点击查看免费下载本篇是 linux-insides-zh 仓库中《Initialization内核初始化》章节的第一部分解读。它承接 Booting引导过程 的最后一步——解压后的内核镜像入口跳转深入剖析 x86_64 内核在进入start_kernel之前执行的初期初始化代码startup_64入口的地址修正、早期四级页表的构建与 Identity 映射、CR4/CR3/EFER/CR0 等控制寄存器的设置、初期内核栈与 GDT 的装载直至跳转到第一个 C 函数x86_64_start_kernel。读完本篇你将完整掌握 Linux 内核从引导程序交棒到 C 代码之间的全部汇编级准备工作并能读懂arch/x86/kernel/head_64.S与arch/x86/kernel/head64.c的关键实现脉络。一、背景引导程序的最后一行代码上一章 是引导过程的最后一个部分。在解压缩完 Linux 内核镜像、并将其妥善放入内存之后内核才开始正式工作。引导程序bootloader 及内核自身的解压代码的任务就是为执行内核代码做准备而本篇要探究的正是内核代码本身在启动 PID 为1的init进程之前所做的大量工作。在引导过程的最后一节中代码跟踪到了arch/x86/boot/compressed/head_64.S文件中的跳转指令jmp *%rax此时rax寄存器中保存的就是 Linux 内核的入口点——该地址是在调用decompress_kernel定义于arch/x86/boot/compressed/misc.c解压内核镜像后计算得到的。由此可见内核引导程序的最后一行代码是一句指向内核入口点的间接跳转。既然入口点地址已经明确我们就可以顺着这条跳转继续探究引导结束后内核做了些什么。解压后的内核镜像入口点定义在arch/x86/kernel/head_64.S该文件的开头几行如下__HEAD .code64 .globl startup_64 startup_64: ... ... ...startup_64过程定义在__HEAD区段下。__HEAD只是一个宏它会展开为可执行的.head.text区段#define __HEAD .section .head.text,ax该区段出现在arch/x86/kernel/vmlinux.lds.S链接器脚本中.text : AT(ADDR(.text) - LOAD_OFFSET) { _text .; ... ... ... } :text 0x9090除了.text区段的定义我们还能从这个脚本文件中得知内核的默认物理地址与虚拟地址。_text是地址计数器对于 x86_64 来说它定义为. __START_KERNEL;__START_KERNEL宏定义在arch/x86/include/asm/page_types.h头文件中由内核映射的虚拟基址与物理起始点相加得到#define _START_KERNEL (__START_KERNEL_map __PHYSICAL_START) #define __PHYSICAL_START ALIGN(CONFIG_PHYSICAL_START, CONFIG_PHYSICAL_ALIGN)换句话说默认情况下Linux 内核的物理基址0x100000016 MBLinux 内核的虚拟基址0xffffffff81000000。这两个数值就是startup_64过程编译链接时的默认物理地址与虚拟地址。二、内核入口的第一步地址计算与合法性检查虽然startup_64编译后默认位于物理地址0x1000000但实际加载地址仍可能发生变化——例如启用 kASLR内核地址空间布局随机化 时。因此内核入口的第一件事就是计算实际加载地址与编译默认地址之间的差值代码如下leaq _text(%rip), %rbp subq $_text - __START_KERNEL_map, %rbp第一行将 RIP 相对地址rip-relative_text放入rbp寄存器第二行从中减去$_text - __START_KERNEL_map。已知_text编译后的默认虚拟地址为0xffffffff81000000物理地址为0x1000000__START_KERNEL_map宏展开为0xffffffff80000000。于是第二行汇编代码对应的表达式为rbp 0x1000000 - (0xffffffff81000000 - 0xffffffff80000000)计算过后rbp的值为0它代表实际加载地址与编译默认地址之间的差值。在我们的例子中0表示内核被加载到了默认地址并且没有启用 kASLR。此后所有需要按实际加载位置修正的符号都依赖这个rbp差值。得到startup_64的地址后需要检查该地址是否已正确对齐。下面的代码执行这一检查testl $~PMD_PAGE_MASK, %ebp jnz bad_address这里将rbp寄存器的低 32 位与PMD_PAGE_MASK比较。PMD_PAGE_MASK代表中层页目录Page Middle DirectoryPMD的屏蔽位其定义如下关于分页的详细理论可阅读分页一章#define PMD_PAGE_MASK (~(PMD_PAGE_SIZE-1)) #define PMD_PAGE_SIZE (_AC(1, UL) PMD_SHIFT) #define PMD_SHIFT 21可以很容易得出PMD_PAGE_SIZE为2 MB。这里采用标准公式检查对齐如果_text的地址没有对齐到 2 MB就跳转到bad_address。在此之后通过检查高 18 位来防止地址过大leaq _text(%rip), %rax shrq $MAX_PHYSMEM_BITS, %rax jnz bad_address该地址必须不超过 46 个比特即小于 2 的 46 次方#define MAX_PHYSMEM_BITS 46完成这些初步检查后内核才能继续进行后续工作。三、修正页表基地址在开始设置 Identity 分页之前需要先修正以下地址——如果startup_64的实际加载值不是默认的0x1000000则包括early_level4_pgt、level3_kernel_pgt在内的很多地址都会不正确addq %rbp, early_level4_pgt (L4_START_KERNEL*8)(%rip) addq %rbp, level3_kernel_pgt (510*8)(%rip) addq %rbp, level3_kernel_pgt (511*8)(%rip) addq %rbp, level2_fixmap_pgt (506*8)(%rip)rbp寄存器中保存的正是前面计算出的相对差值因此把它与early_level4_pgt、level3_kernel_pgt以及level2_fixmap_pgt中特定的表项相加即可得到正确的地址。先来看这些页表的定义NEXT_PAGE(early_level4_pgt) .fill 511,8,0 .quad level3_kernel_pgt - __START_KERNEL_map _PAGE_TABLE NEXT_PAGE(level3_kernel_pgt) .fill L3_START_KERNEL,8,0 .quad level2_kernel_pgt - __START_KERNEL_map _KERNPG_TABLE .quad level2_fixmap_pgt - __START_KERNEL_map _PAGE_TABLE NEXT_PAGE(level2_kernel_pgt) PMDS(0, __PAGE_KERNEL_LARGE_EXEC, KERNEL_IMAGE_SIZE/PMD_SIZE) NEXT_PAGE(level2_fixmap_pgt) .fill 506,8,0 .quad level1_fixmap_pgt - __START_KERNEL_map _PAGE_TABLE .fill 5,8,0 NEXT_PAGE(level1_fixmap_pgt) .fill 512,8,0这些代码看起来难懂实则不然。先看early_level4_pgt早期顶层全局页目录它的前(4096 - 8)个字节全为0即前 511 项均不使用最后一项是level3_kernel_pgt - __START_KERNEL_map _PAGE_TABLE。由于__START_KERNEL_map是内核的虚拟基地址减去它之后就得到了level3_kernel_pgt的物理地址。而_PAGE_TABLE是页表项的访问权限位#define _PAGE_TABLE (_PAGE_PRESENT | _PAGE_RW | _PAGE_USER | \ _PAGE_ACCESSED | _PAGE_DIRTY)level3_kernel_pgt上层页目录中保存的两项用来映射内核空间其前510即L3_START_KERNEL项均为0。L3_START_KERNEL保存的是上层页目录Page Upper Directory中包含__START_KERNEL_map地址的那一条索引它等于510。后面一项level2_kernel_pgt - __START_KERNEL_map _KERNPG_TABLE中的level2_kernel_pgt是一条页表项包含指向中层页目录的指针用来映射内核空间访问权限为#define _KERNPG_TABLE (_PAGE_PRESENT | _PAGE_RW | _PAGE_ACCESSED | \ _PAGE_DIRTY)level2_fixmap_pgtfixmap 映射目录是一组虚拟地址它们可以在内核空间中指向任意的物理地址。它以level2_fixmap_pgt为入口点用 10 MB 大小的空间为 vsyscalls 做映射。level2_kernel_pgt则调用PMDS宏在__START_KERNEL_map地址处为内核的.text创建了 512 MB 大小的空间这 512 MB 空间的后面是模块内存空间。结合 Theory/linux-theory-1.md 中给出的 x86_64 虚拟内存映射可以印证这套布局ffffffff80000000 - ffffffffa0000000正是 512 MB 的 kernel text mapping从物理 0 开始其上是约 1525 MB 的模块映射空间再往上是 vsyscalls 区域。现在回到本节开头的四行修正代码。rbp保存的是实际地址与链接时startup_64地址之差因此只要把它与各个页表项的基地址相加就能得到正确地址。换句话说页表之间的指向关系如下early_level4_pgt[511] - level3_kernel_pgt[0] level3_kernel_pgt[510] - level2_kernel_pgt[0] level3_kernel_pgt[511] - level2_fixmap_pgt[0] level2_kernel_pgt[0] - 512 MB kernel mapping level2_fixmap_pgt[507] - level1_fixmap_pgt即early_level4_pgt的最后一项指向level3_kernel_pgtlevel3_kernel_pgt的最后两项分别指向level2_kernel_pgt和level2_fixmap_pgtlevel2_fixmap_pgt的第 507 项指向level1_fixmap_pgt页目录。需要注意这里修正的只是页表项中保存的下一级页表基地址并不是在构造、填充页目录结构本身——真正的填充工作在接下来的 Identity 映射阶段完成。四、构建 Identity Map 分页现在进入初期页表的 Identity 映射恒等映射初始化过程。在 Identity 映射中虚拟地址会被映射到与自身数值相同的物理地址上即 1 : 1 映射这是内核在开启分页但尚未建立完整内核映射时的过渡手段。首先找到_text与early_level4_pgt的 RIP 相对地址分别放入rdi与rbx寄存器leaq _text(%rip), %rdi leaq early_level4_pgt(%rip), %rbx随后用rax保存_text的地址。全局页目录表中有一条记录存放的正是_text的地址为了得到这条索引把_text的地址右移PGDIR_SHIFT位movq %rdi, %rax shrq $PGDIR_SHIFT, %rax leaq (4096 _KERNPG_TABLE)(%rbx), %rdx movq %rdx, 0(%rbx,%rax,8) movq %rdx, 8(%rbx,%rax,8)PGDIR_SHIFT为39表示虚拟地址中全局页目录位PGD 索引所占的位移。下面的宏定义了各级页目录的索引位移#define PGDIR_SHIFT 39 #define PUD_SHIFT 30 #define PMD_SHIFT 21此后将level3_kernel_pgt的地址放进rdx访问权限设为_KERNPG_TABLE见上文然后把level3_kernel_pgt填入early_level4_pgt的两项中0(%rbx,%rax,8)与8(%rbx,%rax,8)跨越相邻两个全局页目录项。接下来给rdx寄存器加上4096即early_level4_pgt一页的大小并把rdi中的_text物理地址赋给rax然后向上层页目录中写入两个表项addq $4096, %rdx movq %rdi, %rax shrq $PUD_SHIFT, %rax andl $(PTRS_PER_PUD-1), %eax movq %rdx, 4096(%rbx,%rax,8) incl %eax andl $(PTRS_PER_PUD-1), %eax movq %rdx, 4096(%rbx,%rax,8)这里通过shrq $PUD_SHIFT提取_text在上层页目录中的索引andl $(PTRS_PER_PUD-1)保证索引不越界PTRS_PER_PUD为 512并把下一级页目录地址level2_kernel_pgt位于early_level4_pgt之后偏移 4096 字节处写入level3_kernel_pgt对应的两个相邻表项。下一步把中层页目录表项的地址写入level2_kernel_pgt然后修正内核 text 和 data 的虚拟地址leaq level2_kernel_pgt(%rip), %rdi leaq 4096(%rdi), %r8 1: testq $1, 0(%rdi) jz 2f addq %rbp, 0(%rdi) 2: addq $8, %rdi cmp %r8, %rdi jne 1b这里先把level2_kernel_pgt的地址赋给rdi把末项地址赋给r8即rdi 4096。随后循环检查level2_kernel_pgt中每一项的存在位testq $1, 0(%rdi)如果该项有效低位为 1就把rbp差值加到该项上完成地址修正否则跳过。每轮迭代将rdi加 8 指向下一项直到与r8相等遍历完 512 项才结束循环。接下来使用rbp修正phys_base物理地址将early_level4_pgt的物理地址装入rax并跳转至标签1addq %rbp, phys_base(%rip) movq $(early_level4_pgt - __START_KERNEL_map), %rax jmp 1f其中phys_base与level2_kernel_pgt第一项相同表示 512 MB 的内核映射。五、跳转至内核入口点之前的最后准备5.1 开启 PAE/PGE 并装载 CR3跳转到标签1后开启PAEPhysical Address Extension与PGEPaging Global Extension并把phys_base的物理地址见上放入rax同时写入cr3寄存器1: movl $(X86_CR4_PAE | X86_CR4_PGE), %ecx movq %rcx, %cr4 addq phys_base(%rip), %rax movq %rax, %cr3cr3在 x86_64 分页机制中存放的是顶层分页结构Page Global Directory的物理地址其地址字段位于第 12~51 位这与 Theory/linux-theory-1.md 中描述的四级分页结构完全对应CPU 借助cr3找到顶层页目录再按线性地址各段位移逐级索引最终获得物理页框与页内偏移。5.2 检查 NX 位并配置 EFER接下来检查 CPU 是否支持 NXNo-eXecute不可执行位movl $0x80000001, %eax cpuid movl %edx,%edi先将0x80000001放入eax执行cpuid指令获取处理器扩展信息结果存入edx再转移到edi。随后把MSR_EFER即0xc0000080放入ecx执行rdmsr读取 CPU 的 Model Specific RegisterMSRmovl $MSR_EFER, %ecx rdmsr返回值存放于edx:eax。EFER各个位的含义如下SCE在第 0 位LME在第 8 位LMA在第 10 位NXE在第 11 位SVME在第 12 位FFXSR在第 14 位TCE在第 15 位其余位保留31 16 15 14 13 12 11 10 9 8 7 1 0 -------------------------------------------------------------------------------- | | T | | | | | | | | | | | Reserved MBZ | C | FFXSR | LMSLE |SVME|NXE|LMA|MBZ|LME|RAZ|SCE| | | E | | | | | | | | | | --------------------------------------------------------------------------------本篇不逐一解释每个位的含义未涉及到的位与其他 MSR 将在专门章节介绍。把EFER读入edx:eax后通过btsl将_EFER_SCE第 0 位置 1——设置SCE位将启用SYSCALL与SYSRET指令。下一步检查edicpuid 结果中的第 20 位如果该位即 NX 位置位就同时把EFER_NX写入 MSRbtsl $_EFER_SCE, %eax btl $20,%edi jnc 1f btsl $_EFER_NX, %eax btsq $_PAGE_BIT_NX,early_pmd_flags(%rip) 1: wrmsr注意当 CPU 支持 NX 时除了设置EFER.NXE还会把_PAGE_BIT_NX写入early_pmd_flags以便后续构造页表时直接使用 NX 位标记不可执行页。5.3 设置 CR0 控制寄存器在设置完 NX 后还要对cr0控制寄存器中的一些位进行设置X86_CR0_PE- 系统处于保护模式X86_CR0_MP- 与 CR0 的 TS 标志位一同控制 WAIT/FWAIT 指令的功能X86_CR0_ET- 386 允许指定外部数学协处理器为 80287 或 80387X86_CR0_NE- 如果置位则启用内置的 x87 浮点错误报告否则启用 PC 风格的 x87 错误检测X86_CR0_WP- 如果置位则 CPU 在特权等级为 0 时无法写入只读内存页X86_CR0_AM- 当 AM 位置位、EFLAGS 中的 AC 位置位、特权等级为 3 时进行对齐检查X86_CR0_PG- 启用分页。#define CR0_STATE (X86_CR0_PE | X86_CR0_MP | X86_CR0_ET | \ X86_CR0_NE | X86_CR0_WP | X86_CR0_AM | \ X86_CR0_PG) movl $CR0_STATE, %eax movq %rax, %cr0至此分页PG与保护模式PE正式启用内核进入了以页表转换为基础的现代寻址模式。5.4 建立初期内核栈为了从汇编代码顺利转入 C 语言代码内核需要一个可用的栈。首先将栈指针指向内存中合适的区域然后重置 FLAGS 寄存器movq stack_start(%rip), %rsp pushq $0 popfq这里最有意思的地方在于stack_start它也定义在当前的源文件arch/x86/kernel/head_64.S中GLOBAL(stack_start) .quad init_thread_unionTHREAD_SIZE-8GLOBAL宏我们应当很熟悉它定义在arch/x86/include/asm/linkage.h头文件中#define GLOBAL(name) \ .globl name; \ name:THREAD_SIZE定义在arch/x86/include/asm/page_64_types.h它依赖于KASAN_STACK_ORDER的值#define THREAD_SIZE_ORDER (2 KASAN_STACK_ORDER) #define THREAD_SIZE (PAGE_SIZE THREAD_SIZE_ORDER)先考虑禁用了 KASAN 且PAGE_SIZE为 4096 的情况此时THREAD_SIZE为16 KB代表一个线程内核栈的大小。为什么是线程每一个进程都可能拥有父进程和子进程事实上父进程和子进程使用不同的栈空间每一个新进程都会拥有一个新的内核栈。在 Linux 内核中这个栈由thread_info结构中的一个 union 表示union thread_union { struct thread_info thread_info; unsigned long stack[THREAD_SIZE/sizeof(long)]; };例如init_thread_union定义如下union thread_union init_thread_union __init_task_data { INIT_THREAD_INFO(init_task) };其中INIT_THREAD_INFO接受task_struct结构类型的参数并进行一些初始化操作#define INIT_THREAD_INFO(tsk) \ { \ .task tsk, \ .flags 0, \ .cpu 0, \ .addr_limit KERNEL_DS, \ }task_struct结构在内核中代表对进程的描述。因此thread_union包含了关于一个进程的低级信息并且位于其内核栈底部整体布局如下----------------------- | | | | | | | Kernel stack | | | | | | | |-----------------------| | | | struct thread_info | | | -----------------------注意栈顶保留了 8 个字节的空间用于保护对下一个内存页的非法访问这正是stack_start中init_thread_union THREAD_SIZE - 8减 8 的原因。5.5 加载新的 GDT初期启动栈设置好之后使用lgdt指令更新全局描述符表GDTlgdt early_gdt_descr(%rip)其中early_gdt_descr定义如下early_gdt_descr: .word GDT_ENTRIES*8-1 early_gdt_descr_base: .quad INIT_PER_CPU_VAR(gdt_page)需要重新加载 GDT 的原因是虽然目前内核仍工作于用户空间的低地址中但很快它将在自己的内存地址空间中运行。early_gdt_descr中的全局描述符表包含 32 项用于内核代码、数据、线程局部存储段等#define GDT_ENTRIES 32再来看early_gdt_descr_base。首先gdt_page的定义在arch/x86/include/asm/desc.h中struct gdt_page { struct desc_struct gdt[GDT_ENTRIES]; } __attribute__((aligned(PAGE_SIZE)));它只包含一项desc_struct的数组gdt。desc_struct定义如下struct desc_struct { union { struct { unsigned int a; unsigned int b; }; struct { u16 limit0; u16 base0; unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1; unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8; }; }; } __attribute__((packed));它跟 GDT 描述符的位布局很像。同时注意gdt_page结构是PAGE_SIZE4096对齐的即gdt将占用一页内存。再看INIT_PER_CPU_VAR它定义在arch/x86/include/asm/percpu.h作用只是把给定参数与init_per_cpu__前缀连接起来#define INIT_PER_CPU_VAR(var) init_per_cpu__##var宏展开后得到init_per_cpu__gdt_page。而在链接器脚本arch/x86/kernel/vmlinux.lds.S中可以发现#define INIT_PER_CPU(x) init_per_cpu__##x x __per_cpu_load INIT_PER_CPU(gdt_page);INIT_PER_CPU展开后同样得到init_per_cpu__gdt_page并将其值设置为相对于__per_cpu_load的偏移量。这样我们就得到了新 GDT 的正确基地址。per-CPU 变量是 2.6 内核引入的特性当我们创建一个 per-CPU 变量时每个 CPU 都会拥有一份它自己的拷贝此处创建的就是gdt_pageper-CPU 变量。这种变量的优点是每个 CPU 都只访问自己的拷贝无需加锁。因此在多处理器环境下每一个处理器核心都拥有一份自己的 GDT 表其中的每一项都代表一块内存这块内存可以由在该核心上运行的线程访问。关于 per-CPU 变量的更详细介绍可阅读 Concepts/linux-cpu-1.mdPer-cpu 变量。5.6 重新加载段寄存器与 GS 基址加载好新的全局描述符表之后与之前一样重新加载各个段寄存器xorl %eax,%eax movl %eax,%ds movl %eax,%ss movl %eax,%es movl %eax,%fs movl %eax,%gs在所有这些步骤结束后还需要设置gs寄存器令它指向一个特殊的栈irqstack用于处理中断movl $MSR_GS_BASE,%ecx movl initial_gs(%rip),%eax movl initial_gs4(%rip),%edx wrmsr其中MSR_GS_BASE为#define MSR_GS_BASE 0xc0000101需要把MSR_GS_BASE放入ecx寄存器并利用wrmsr指令把eax、edx中的数据即initial_gs指向的地址写入 MSR。在 64 位模式下cs、fs、ds、ss段寄存器不再用于寻址但fs和gs仍可使用——它们有一个隐含的部分与实模式下的cs段寄存器类似这个隐含部分存储了一个描述符指向 Model Specific RegisterMSR。因此0xc0000101正是gs.base的 MSR 地址。当发生系统调用或中断时入口点处并没有内核栈所以MSR_GS_BASE会被用来存放中断栈。5.7 跳转到第一个 C 函数接下来把实模式 bootparam 结构的地址放入rdi注意rsi从一开始就保存了该结构体的指针rdi在进入 C 前被置为实模式数据地址然后跳转到 C 语言代码movq initial_code(%rip),%rax pushq $0 pushq $__KERNEL_CS pushq %rax lretq这里把initial_code放入rax并依次向栈中压入一个无用的地址、__KERNEL_CS和initial_code的地址。随后的lretq指令从栈上弹出返回地址并跳转过去。initial_code同样定义在这个文件里.balign 8 GLOBAL(initial_code) .quad x86_64_start_kernel ... ... ...可以看到initial_code保存的是x86_64_start_kernel的地址其定义在arch/x86/kernel/head64.casmlinkage __visible void __init x86_64_start_kernel(char * real_mode_data) { ... ... ... }这个函数接受一个参数real_mode_data即前面保存到rdi寄存器中的实模式数据地址。它是内核中第一个执行的 C 语言代码六、走进 x86_64_start_kernelstart_kernel 前的最后准备在真正到达内核入口点——init/main.c中的start_kernel函数之前还有最后的准备工作。首先x86_64_start_kernel函数中有一系列编译期检查BUILD_BUG_ON(MODULES_VADDR __START_KERNEL_map); BUILD_BUG_ON(MODULES_VADDR - __START_KERNEL_map KERNEL_IMAGE_SIZE); BUILD_BUG_ON(MODULES_LEN KERNEL_IMAGE_SIZE 2*PUD_SIZE); BUILD_BUG_ON((__START_KERNEL_map ~PMD_MASK) ! 0); BUILD_BUG_ON((MODULES_VADDR ~PMD_MASK) ! 0); BUILD_BUG_ON(!(MODULES_VADDR __START_KERNEL)); BUILD_BUG_ON(!(((MODULES_END - 1) PGDIR_MASK) (__START_KERNEL PGDIR_MASK))); BUILD_BUG_ON(__fix_to_virt(__end_of_fixed_addresses) MODULES_END);这些检查包括模块的虚拟地址不能低于内核 text 段基地址__START_KERNEL_map包含模块的内核 text 段空间大小不能小于内核镜像大小内核映射与模块区必须与 PMD 对齐模块区必须位于内核映射之后且与内核映射落在同一个 PGD 条目内等等。BUILD_BUG_ON宏定义如下#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))这个设计的原理很巧妙。以第一个条件MODULES_VADDR __START_KERNEL_map为例!!condition等价于condition ! 0——若条件为真则为 1否则为 0乘以 2 后变成2或0。于是1 - 2*!!(condition)为-1条件成立或1条件不成立。当条件成立时char[-1]数组长度非法sizeof表达式在编译期直接报错条件不成立时则是一个合法的长度为 1 的char数组编译通过。通过这种让非法常量触发编译错误的技巧内核把布局假设固化成了编译期契约。接下来start_kernel之前还会调用cr4_init_shadow函数为每个 CPU 保存cr4的 Shadow Copy。上下文切换可能会修改cr4中的位因此需要保存每个 CPU 中cr4的内容。在这之后调用reset_early_page_tables函数它重置所有的全局页目录项并向cr3重新写入全局页目录表的地址for (i 0; i PTRS_PER_PGD-1; i) early_level4_pgt[i].pgd 0; next_early_pgt 0; write_cr3(__pa_nodebug(early_level4_pgt));很快我们就会设置新的页表。这里遍历了所有的全局页目录项PTRS_PER_PGD为 512将其清零然后把next_early_pgt置为 0并把early_level4_pgt的物理地址写入cr3。__pa_nodebug是一个宏展开为((unsigned long)(x) - __START_KERNEL_map phys_base)即通过虚拟地址减去内核映射基址再加上实际物理基址偏移得到物理地址。next_early_pgt的用途会在后续篇章展开——在 Initialization/linux-initialization-2.md 中可以看到它被用作early_dynamic_pgts动态页表缓冲区的下标EARLY_DYNAMIC_PAGE_TABLES为 64当固定页表不够用时早期缺页处理程序会据此动态分配新的页表。此后内核清空从__bss_stop到__bss_start的_bss段下一步将是建立初期 IDT中断描述符表的处理代码——内容很多留待下一部分探究。七、总结与延伸阅读第一部分关于 Linux 内核初始化过程的旅程到这里就结束了。回顾一下这条清晰的主线引导程序通过jmp *%rax把控制权交给解压后的内核入口startup_64arch/x86/kernel/head_64.Sstartup_64计算实际加载地址与链接地址的差值完成 2 MB 对齐与地址上限检查修正early_level4_pgt、level3_kernel_pgt、level2_fixmap_pgt中保存的下一级页表基地址构建 Identity 映射把_text所在的物理地址 1:1 映射进早期四级页表设置CR4.PAE|PGE、装载CR3、检测 NX 并配置EFER.SCE/NXE、设置CR0启用保护模式与分页建立初期内核栈init_thread_union、加载新 GDTper-CPU 的gdt_page、重设段寄存器与GS_BASE通过lretq跳转到第一个 C 函数x86_64_start_kernel并在其中完成编译期布局校验、cr4_init_shadow与reset_early_page_tables为start_kernel铺路。下一部分Initialization/linux-initialization-2.md会看到初期中断处理程序的初始化过程、早期缺页处理函数以及内核空间内存映射等内容。本系列的完整目录与各篇定位可参见 Initialization/README.md。相关链接分页理论四级页表、页表项布局、x86_64 虚拟内存映射Theory/linux-theory-1.md上一部分——内核解压Booting/linux-bootstrap-5.mdper-CPU 变量详解Concepts/linux-cpu-1.md后续部分——早期中断与异常控制Initialization/linux-initialization-2.mdModel Specific RegisterMSR与 NX 位、ASLR 等概念可结合本系列其他篇章与 Intel 手册进一步研读。赞分享文档教程操作系统【免费下载链接】linux-insides-zhLinux 内核揭秘项目地址https://gitcode.com/gh_mirrors/li/linux-insides-zh点击查看免费下载相关推荐Linux 内核初始化详解一从 startup_64 汇编入口到 start_kernel 的早期启动流程Linux 内核初始化详解一从 startup_64 汇编入口到 start_kernel 的早期启动流程 本文是 linux insides zh 仓库Linux 内核初始化一x86_64 内核入口点、GDT 与早期页表构建全解析Linux 内核初始化一x86_64 内核入口点、GDT 与早期页表构建全解析 导读 本篇是 Linux 内核初始化章节的第一部分承接 Linux 内核文档教程操作系统linux-insides 解读四Linux x86_64 内核非早期中断门初始化与 trap_init 全流程linux insides 解读四Linux x86_64 内核非早期中断门初始化与 trap_init 全流程 本文是 linux insides 开源文档教程操作系统上一篇浏览器操作 GitHub 网页版4 个阶段跑通协作闭环下一篇在 Exoscale 上部署 Kubernetes Cluster AutoscalerSKS Nodepool 与 Instance Pool 的自动扩缩容实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考