当前位置: 首页 > article >正文

Linux内核,内存分布

x86_64的物理地址范围为64bit,但是因为地址空间太大目前不可能完全用完,当前支持57bit和48bit两种虚拟地址模式。

地址模式单个空间用户地址空间内核地址空间
32位2G0x00000000 - 0x7FFFFFFF0x80000000 - 0xFFFFFFFF
64位(48bit)128T0x00000000 00000000 - 0x00007FFF FFFFFFFF0xFFFF8000 00000000 - 0xFFFFFFFF FFFFFFFF
64位(57bit)64P0x00000000 00000000 - 0x00FFFFFF FFFFFFFF0xFF000000 00000000 - 0xFFFFFFFF FFFFFFFF

48bit模式的地址空间布局(4级页表)

Start addrOffsetEnd addrSizeVM area description描述
0000000000000000000007fffffffffff128 TBuser-space virtual memory, different per mm用户地址空间,每个进程mm指向的都不同
0000800000000000+128 TBffff7fffffffffff~16M TB… huge, almost 64 bits wide hole of non-canonical virtual memory addresses up to the -128 TB starting offset of kernel mappings.巨大空洞
Kernel-space virtual memory, shared between all processes:以下为内核地址空间:
ffff800000000000-128 TBffff87ffffffffff8 TB… guard hole, also reserved for hypervisor
ffff880000000000-120 TBffff887fffffffff0.5 TBLDT remap for PTILDT(Local Descriptor Table):局部描述符表KPTI(Kernel page-table isolation):内核页表隔离
ffff888000000000-119.5 TBffffc87fffffffff64 TBdirect mapping of all physical memory (page_offset_base)线性映射的区域
0000000000000000-55.5 TBffffc8ffffffffff0.5 TB… unused hole
ffffc90000000000-55 TBffffe8ffffffffff32 TBvmalloc/ioremap space (vmalloc_base)vmalloc和ioremap空间
ffffe90000000000-23 TBffffe9ffffffffff1 TB… unused hole
ffffea0000000000-22 TBffffeaffffffffff1 TBvirtual memory map (vmemmap_base)page结构存储的位置
ffffeb0000000000-21 TBffffebffffffffff1 TB… unused hole
ffffec0000000000-20 TBfffffbffffffffff16 TBKASAN shadow memoryKASAN影子内存
Identical layout to the 56-bit one from here on:从这里开始,与56-bit布局相同:
fffffc0000000000000007fffffffffff128 TBuser-space virtual memory, different per mm用户地址空间,每个进程mm指向的都不同
0000000000000000-4 TBfffffdffffffffff2 TB… unused hole
vaddr_end for KASLR
fffffe0000000000-2 TBfffffe7fffffffff0.5 TBcpu_entry_area mapping
fffffe8000000000-1.5 TBfffffeffffffffff0.5 TB… unused hole
ffffff0000000000-1 TBffffff7fffffffff0.5 TB%esp fixup stacks
ffffff8000000000-512 GBffffffeeffffffff444 GB… unused hole
ffffffef00000000-68 GBfffffffeffffffff64 GBEFI region mapping space
ffffffff00000000-4 GBffffffff7fffffff2 GB… unused hole
ffffffff80000000-2 GBffffffff9fffffff512 MBkernel text mapping, mapped to physical address 0内核代码区域
ffffffff80000000-2048 MB
ffffffffa0000000-1536 MBfffffffffeffffff1520 MBmodule mapping space模块加载区域
ffffffffff000000-16 MB
FIXADDR_START~-11 MBffffffffff5fffff~0.5 MBkernel-internal fixmap range, variable size and offset
ffffffffff600000-10 MBffffffffff600fff4 kBlegacy vsyscall ABI
fffffffffffe00000-2 MBfffffffffffffffff2 MB… unused hole

其中重点区域的说明:

direct mapping:直接映射覆盖系统中的所有内存,直至最高内存地址(这意味着在某些情况下,它还可以包括PCI内 memory)。
vmalloc space:vmalloc空间也是lazy策略的,使用page_fault机制来延后分配,使用init_top_pgt作为参考。
EFI region:我们将EFI运行时服务映射到64Gb大型虚拟内存窗口中的“ efi_pgd” PGD中(此大小是任意的,以后可以根据需要提高)。映射不是任何其他内核PGD的一部分,并且仅在EFI运行时期间可用。
KASLR:请注意,如果启用CONFIG_RANDOMIZE_MEMORY,则将随机化所有物理内存,直接映射物理内存空间(direct mapping)、vmalloc/ioremap空间和虚拟内存映射。它们的顺序被保留,但是它们在启动时加上基础偏移。在此处进行任何更改时,请务必对KASLR格外小心。除KASAN阴影区域外,KASLR地址范围不得与其他区域重叠。因此KASAN为了保证正确会禁用KASLR。

57bit模式的地址空间布局(5级页表)

在这里插入图片描述

内核页表初始化

decompress阶段

head_64.S和head64.c

early_top_pgt

内核代码在跳转到start_kernel()以前,运行在head_64.S和head64.c中,此时使用一个临时页表early_top_pgt来做虚拟地址到物理地址的转换:

SYM_DATA_START_PTI_ALIGNED(early_top_pgt)
	.fill	512,8,0
	.fill	PTI_USER_PGD_FILL,8,0
SYM_DATA_END(early_top_pgt)

SYM_DATA_START_PTI_ALIGNED(early_top_pgt):声明符号early_top_pgt,并要求按PTI对齐规则(通常为4K对齐)。
.fill 512,8,0:填充512个8字节的条目,初始值为0。这对应x86-64四级分页中的顶级页表(PML4),每个条目占用8字节,共512项。
.fill PTI_USER_PGD_FILL,8,0:进一步填充用户空间相关的页表条目。PTI_USER_PGD_FILL是一个宏,表示需要隔离的用户空间条目数量。例如,内核可能保留部分条目供用户态隔离使用,防止通过侧信道攻击访问内核数据。

代码部分功能描述
SYM_DATA_START_PTI_ALIGNED(…)定义对齐的页表起始位置,确保分页结构符合硬件要求。
.fill 512,8,0初始化PML4表,覆盖所有可能的条目,为后续映射预留空间。
.fill PTI_USER_PGD_FILL,8,0按PTI要求隔离用户空间条目,缓解Meltdown等侧信道攻击。

init_top_pgt

#if defined(CONFIG_XEN_PV) || defined(CONFIG_PVH)
SYM_DATA_START_PTI_ALIGNED(init_top_pgt)
    .quad level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE_NOENC  ; 直接映射条目
    .org init_top_pgt + L4_PAGE_OFFSET*8, 0                            ; 清零用户空间条目
    .quad level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE_NOENC  ; 可能的兼容性设置
    .org init_top_pgt + L4_START_KERNEL*8, 0                           ; 定位到内核空间条目
    .quad level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE_NOENC   ; 内核虚拟映射
    .fill PTI_USER_PGD_FILL,8,0                                        ; 填充剩余用户条目为0
SYM_DATA_END(init_top_pgt)
/*
 * 定义页上级目录(Page Upper Directory, PUD),即level3_ident_pgt
 * 该页表用于内核启动初期的直接映射(物理地址=虚拟地址)
 * SYM_DATA_START_PAGE_ALIGNED 确保符号按页(4096字节)对齐
 */
SYM_DATA_START_PAGE_ALIGNED(level3_ident_pgt)
    /*
     * 第一个PUD条目指向level2_ident_pgt(PMD)
     * 计算方式:level2_ident_pgt的物理地址 + 页表属性
     * 
     * 关键分解:
     * 1. level2_ident_pgt - __START_KERNEL_map:
     *    __START_KERNEL_map 是内核虚拟地址空间起始地址(如0xffffffff80000000)
     *    通过减去该值,将虚拟地址转换为物理地址(直接映射阶段)
     * 2. _KERNPG_TABLE_NOENC:
     *    页表项属性:存在位 | 可读写 | 内核权限 | 无内存加密
     *    值通常为 0x003(_PAGE_PRESENT | _PAGE_RW | _PAGE_ACCESSED)
     */
    .quad   level2_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE_NOENC
    
    /*
     * 填充剩余的511个PUD条目为0
     * 原因:
     * 1. 每个页表层级(PUD/PMD等)有512个条目
     * 2. 此处仅需映射第一个1GB物理内存(由后续level2_ident_pgt配置)
     * 3. 其他条目保持未映射状态(安全且节省空间)
     */
    .fill   511, 8, 0  /* 511 entries x 8 bytes = 4088 bytes */
SYM_DATA_END(level3_ident_pgt)  /* 结束符号定义 */


/*
 * 定义页中间目录(Page Middle Directory, PMD),即level2_ident_pgt
 * 该页表负责映射前1GB物理内存,使用大页(2MB或1GB)提升效率
 */
SYM_DATA_START_PAGE_ALIGNED(level2_ident_pgt)
    /*
     * PMDS宏:生成连续的PMD条目以映射连续物理内存
     * 参数解析:
     * @0: 起始物理地址(0表示从物理地址0开始映射)
     * @__PAGE_KERNEL_IDENT_LARGE_EXEC: 页表属性(大页+可执行)
     * @PTRS_PER_PMD: 条目数量(通常为512,覆盖完整PMD)
     *
     * 属性详解:
     * __PAGE_KERNEL_IDENT_LARGE_EXEC 包含:
     * - _PAGE_PRESENT: 页存在
     * - _PAGE_RW: 可读写
     * - _PAGE_ACCESSED: 已访问
     * - _PAGE_DIRTY: 脏页
     * - _PAGE_LARGE: 使用大页(2MB或1GB)
     * - _PAGE_GLOBAL: 全局页(TLB不刷新)
     * - _PAGE_EXEC: 允许代码执行(未设置NX位)
     *
     * 注释说明:
     * 1. 显式设置_PAGE_GLOBAL,即使CPU可能忽略该标志(兼容性考虑)
     * 2. 不设置NX(No Execute)位,允许从此区域执行代码
     * 3. 映射前1GB物理内存(512 entries x 2MB = 1GB)
     */
    PMDS(0, __PAGE_KERNEL_IDENT_LARGE_EXEC, PTRS_PER_PMD)
SYM_DATA_END(level2_ident_pgt)  /* 结束符号定义 */
SYM_DATA_START_PAGE_ALIGNED(level2_fixmap_pgt)
	.fill	(512 - 4 - FIXMAP_PMD_NUM),8,0
	pgtno = 0
	.rept (FIXMAP_PMD_NUM)
	.quad level1_fixmap_pgt + (pgtno << PAGE_SHIFT) - __START_KERNEL_map \
		+ _PAGE_TABLE_NOENC;
	pgtno = pgtno + 1
	.endr
	/* 6 MB reserved space + a 2MB hole */
	.fill	4,8,0
SYM_DATA_END(level2_fixmap_pgt)

SYM_DATA_START_PAGE_ALIGNED(level1_fixmap_pgt)
	.rept (FIXMAP_PMD_NUM)
	.fill	512,8,0
	.endr
SYM_DATA_END(level1_fixmap_pgt)

/*
 * level2_fixmap_pgt:页中间目录(PMD),用于管理固定映射(Fixmap)区域
 * Fixmap 是内核中用于特殊用途的虚拟地址空间(如临时映射设备内存、APIC 等)
 * SYM_DATA_START_PAGE_ALIGNED 确保该符号按页(4096 字节)对齐
 */
SYM_DATA_START_PAGE_ALIGNED(level2_fixmap_pgt)
    /*
     * 步骤 1:填充无效条目(保留空间)
     * 总条目数 512 - 保留末尾 4 条目 - FIXMAP_PMD_NUM 有效条目
     * 目的:
     * 1. 保留 FIXMAP_PMD_NUM 个 PMD 条目用于 Fixmap
     * 2. 末尾保留 4 个条目(可能与特定硬件或内存布局对齐要求相关)
     * 3. 中间区域填充 0 表示未映射
     */
    .fill   (512 - 4 - FIXMAP_PMD_NUM), 8, 0  /* 填充无效条目 */

    /* 
     * 步骤 2:生成 FIXMAP_PMD_NUM 个有效 PMD 条目
     * 这些条目指向 level1_fixmap_pgt(PTE 表),形成层级结构
     */
    pgtno = 0  /* 初始化页表编号计数器 */
    .rept (FIXMAP_PMD_NUM)  /* 重复生成 FIXMAP_PMD_NUM 次 */
        /*
         * 构造 PMD 条目:
         * 物理地址 = level1_fixmap_pgt + pgtno * PAGE_SIZE - __START_KERNEL_map
         * 属性 = _PAGE_TABLE_NOENC(存在 + 可读可写)
         * 
         * 关键分解:
         * 1. level1_fixmap_pgt 是虚拟地址,需转换为物理地址:
         *    - __START_KERNEL_map 是内核虚拟地址空间起始(如 0xffffffff80000000)
         *    - 减去 __START_KERNEL_map 得到物理地址
         * 2. (pgtno << PAGE_SHIFT):每个 PTE 表占一页(4096 字节),按页偏移
         * 3. _PAGE_TABLE_NOENC:属性标志(0x003,存在 + 可读可写)
         */
        .quad level1_fixmap_pgt + (pgtno << PAGE_SHIFT) - __START_KERNEL_map \
            + _PAGE_TABLE_NOENC
        pgtno = pgtno + 1  /* 递增页表编号 */
    .endr

    /* 步骤 3:保留末尾 4 个 PMD 条目(填充 0) */
    /* 注释提到的 "6 MB reserved space + a 2MB hole" 可能指特定平台的保留区域 */
    .fill   4, 8, 0  /* 填充 4 个无效条目 */
SYM_DATA_END(level2_fixmap_pgt)  /* 结束符号定义 */


/*
 * level1_fixmap_pgt:页表条目(PTE)数组,用于实际物理页映射
 * 每个 PMD 条目指向一个 PTE 表,每个 PTE 表管理 512 个 4KB 页(共 2MB)
 */
SYM_DATA_START_PAGE_ALIGNED(level1_fixmap_pgt)
    /*
     * 初始化 FIXMAP_PMD_NUM 个 PTE 表,每个表 512 个条目,初始为 0
     * 目的:
     * 1. 预留空间供内核运行时动态映射(如 fixmap 机制)
     * 2. 初始时未映射,后续按需设置具体物理地址
     */
    .rept (FIXMAP_PMD_NUM)  /* 重复生成 FIXMAP_PMD_NUM 个 PTE 表 */
        .fill   512, 8, 0  /* 每个 PTE 表 512 条目 × 8 字节 = 4096 字节(一页) */
    .endr
SYM_DATA_END(level1_fixmap_pgt)  /* 结束符号定义 */

下面是关于汇编指令的介绍:
ORG是Origin的缩写:起始地址,源。在汇编语言源程序的开始通常都用一条ORG伪指令来实现规
操作步骤:
计算目标地址:
init_top_pgt + L4_PAGE_OFFSET*8 定位到用户空间地址对应的PML4条目位置。
调整位置计数器:
将汇编器的当前位置计数器($)设置到该目标地址。
填充未初始化区域:
若当前位置与目标地址之间存在间隙(如之前的条目未填满),用0填充这些间隙。
实际效果:
确保用户空间地址对应的PML4条目被显式初始化为0,表示 用户空间无法直接访问内核内存。这是 页表隔离(PTI) 的关键步骤,防止用户程序通过侧信道攻击(如Meltdown)窃取内核数据。

形式:.fill repeat , size , value
其中,repeat、size 和value都是常量表达式。Fill的含义是反复拷贝size个字节。Repeat可以大于等于0。size也可以大于等于0,但不能超过8,如果超过8,也只取8。把repeat个字节以8个为一组,每组的最高4个字节内容为0,最低4字节内容置为value。

程序的起始地址。如果不用ORG规定则汇编得到的目标程序将从0000H开始
.quad 定义八个字节的数据
quad bignums
.quad表示零个或多个bignums(用逗号分隔),对于每个bignum,其缺省值是8字节整数。如果bignum超过8字节,则打印一个警告信息;并只取bignum最低8字节。
在这里插入图片描述

其中主要建立了4块区域的映射:

regionsizedesctipt
identity mapping1G虚拟地址和物理地址相等
direct mapping1G线性映射空间,起始虚拟地址
kernel image512M内核映像映射空间
fixmap固定映射空间

但是在跳转到start_kernel()之前,内核重新构造了init_top_pgt:

SYM_DATA(initial_code,	.quad x86_64_start_kernel)

该行代码用于 定义内核的初始执行地址,将 x86_64_start_kernel 函数的入口地址存储到全局符号 initial_code 中,供内核启动流程使用。

1. 符号定义宏 SYM_DATA

作用:
在 Linux 内核汇编中,SYM_DATA 是一个宏,用于定义全局数据符号(Global Data Symbol)。
它会确保符号的正确对齐(如按 8 字节对齐)并附加必要的元信息(如 ELF 节类型),以便链接器和内核正确识别。

2. initial_code 符号

作用:
存储内核的初始代码入口地址,供启动代码(如汇编启动桩)跳转到内核主函数。

生命周期:

编译阶段:由汇编器生成,存储在目标文件(.o)的数据段中。

链接阶段:链接器将其地址固定在内核镜像的特定位置(如 .data 节)。

运行时:内核启动代码通过读取 initial_code 的值,跳转到 x86_64_start_kernel。

3. .quad x86_64_start_kernel

.quad 指令
在当前位置写入一个 64 位(8 字节)的值。此处值为符号 x86_64_start_kernel 的地址。

x86_64_start_kernel
功能:x86_64 架构的主内核入口函数,负责初始化关键子系统(如内存管理、中断控制)。

定义位置:通常位于 arch/x86/kernel/head64.c 或类似文件中。

执行时机:在完成底层汇编环境初始化(如分页、栈设置)后,由此函数接管控制权。

x86_64_start_kernel

asmlinkage __visible void __init __noreturn x86_64_start_kernel(char * real_mode_data)
{
	/*
	 * Build-time sanity checks on the kernel image and module
	 * area mappings. (these are purely build-time and produce no code)
	 */
	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));
	MAYBE_BUILD_BUG_ON(!(((MODULES_END - 1) & PGDIR_MASK) ==
				(__START_KERNEL & PGDIR_MASK)));
	BUILD_BUG_ON(__fix_to_virt(__end_of_fixed_addresses) <= MODULES_END);

	cr4_init_shadow();

	/* Kill off the identity-map trampoline */
	reset_early_page_tables();

	clear_bss();

	/*
	 * This needs to happen *before* kasan_early_init() because latter maps stuff
	 * into that page.
	 */
	clear_page(init_top_pgt);

	/*
	 * SME support may update early_pmd_flags to include the memory
	 * encryption mask, so it needs to be called before anything
	 * that may generate a page fault.
	 */
	sme_early_init();

	kasan_early_init();

	/*
	 * Flush global TLB entries which could be left over from the trampoline page
	 * table.
	 *
	 * This needs to happen *after* kasan_early_init() as KASAN-enabled .configs
	 * instrument native_write_cr4() so KASAN must be initialized for that
	 * instrumentation to work.
	 */
	__native_tlb_flush_global(this_cpu_read(cpu_tlbstate.cr4));

	idt_setup_early_handler();

	/* Needed before cc_platform_has() can be used for TDX */
	tdx_early_init();

	copy_bootdata(__va(real_mode_data));

	/*
	 * Load microcode early on BSP.
	 */
	load_ucode_bsp();

	/* set init_top_pgt kernel high mapping*/
	init_top_pgt[511] = early_top_pgt[511];

	x86_64_start_reservations(real_mode_data);
}

以下是 x86_64_start_kernel 函数的逐行详细分析,结合 Linux 内核启动流程和 x86_64 架构特性:


1. 函数定义

asmlinkage __visible void __init __noreturn x86_64_start_kernel(char * real_mode_data)
  • asmlinkage: 声明函数参数通过栈传递(x86_64 默认用寄存器,此处兼容旧约定)
  • __visible: 强制符号在目标文件中可见(防止链接器优化)
  • __init: 标记函数仅在内核初始化阶段存在(内存会被回收)
  • __noreturn: 函数不会返回(最终跳转到 start_kernel()
  • real_mode_data: 指向实模式阶段数据的指针(如 boot_params)

2. 编译时内存布局检查

BUILD_BUG_ON(MODULES_VADDR < __START_KERNEL_map);
BUILD_BUG_ON(MODULES_VADDR - __START_KERNEL_map < KERNEL_IMAGE_SIZE);
  • 目的:确保内核模块区域(MODULES_VADDR)位于内核镜像(__START_KERNEL_map)之后,且留有足够空间。
  • 失败后果:编译错误,防止模块与内核代码重叠。
BUILD_BUG_ON(MODULES_LEN + KERNEL_IMAGE_SIZE > 2*PUD_SIZE);
  • 验证内核镜像和模块总大小不超过 2个PUD(Page Upper Directory)的容量(通常 2*1GB = 2GB)。
BUILD_BUG_ON((__START_KERNEL_map & ~PMD_MASK) != 0);
BUILD_BUG_ON((MODULES_VADDR & ~PMD_MASK) != 0);
  • 检查内核和模块的起始地址是否按 PMD边界对齐(2MB 对齐),确保大页映射有效性。
BUILD_BUG_ON(!(MODULES_VADDR > __START_KERNEL));
  • 强制模块区域在内核代码之后,避免地址冲突。
MAYBE_BUILD_BUG_ON(!(((MODULES_END - 1) & PGDIR_MASK) == (__START_KERNEL & PGDIR_MASK)));
  • 确保模块结束地址与内核起始地址在 同一PGD条目(Page Global Directory,512GB范围)内,简化页表管理。
BUILD_BUG_ON(__fix_to_virt(__end_of_fixed_addresses) <= MODULES_END);
  • 检查固定映射区域(Fixmap)是否在模块区域之后,避免地址空间覆盖。

3. 关键初始化操作

(1) 影子CR4初始化
cr4_init_shadow();
  • 作用:为每个CPU初始化影子CR4副本,用于安全地管理CR4寄存器更新(如避免TSX切换漏洞)。
(2) 清除临时页表
reset_early_page_tables();
  • 操作:清空 early_top_pgt 中的早期直接映射(Identity Mapping),释放物理页。
  • 目的:移除启动阶段临时页表,防止非法访问。
(3) 清零BSS段
clear_bss();
  • BSS段:存储未初始化的全局变量(如 static int x;)。
  • 操作:将BSS段内存清零,确保变量初始状态正确。
(4) 清空顶级页表
clear_page(init_top_pgt);
  • init_top_pgt:内核正式使用的顶级页表(PML4)。
  • 目的:清除旧数据,避免残留映射干扰后续初始化。
(5) 内存加密初始化
sme_early_init();
  • SME(Secure Memory Encryption):AMD 内存加密技术。
  • 操作:检测并启用内存加密,更新页表属性(如 early_pmd_flags)。
(6) KASAN初始化
kasan_early_init();
  • KASAN(Kernel Address SANitizer):动态内存错误检测工具。
  • 操作:初始化影子内存(Shadow Memory),拦截非法内存访问。
(7) 刷新全局TLB
__native_tlb_flush_global(this_cpu_read(cpu_tlbstate.cr4));
  • 作用:清除早期页表遗留的TLB缓存条目,确保新页表生效。
  • 时机:必须在KASAN初始化后执行(KASAN会插桩内存操作函数)。

4. 中断与安全初始化

(1) 早期中断处理
idt_setup_early_handler();
  • IDT(Interrupt Descriptor Table):设置早期中断处理函数(如#PF、#DB),用于启动阶段异常处理。
(2) TDX初始化
tdx_early_init();
  • TDX(Trust Domain Extensions):Intel 的可信执行环境扩展。
  • 操作:检测并初始化TDX相关配置,为安全容器做准备。

5. 数据与微码处理

(1) 复制实模式数据
copy_bootdata(__va(real_mode_data));
  • __va: 将实模式数据的物理地址转换为内核虚拟地址。
  • 操作:复制 bootloader 传递的数据(如命令行参数、内存映射)到内核空间。
(2) 加载微码更新
load_ucode_bsp();
  • 微码(Microcode):CPU 固件补丁,修复硬件缺陷。
  • 操作:在BSP(Bootstrap Processor)上加载微码更新。

6. 页表切换与后续流程

init_top_pgt[511] = early_top_pgt[511];
  • 操作:将 early_top_pgt 的内核高映射条目复制到 init_top_pgt
  • 目的:保留内核代码和数据的映射,确保后续代码执行连续。
x86_64_start_reservations(real_mode_data);
  • 后续流程:进入内核保留区域初始化,最终调用 start_kernel()(通用内核入口)。

内存布局演进

阶段页表映射范围作用
早期启动early_top_pgt直接映射(1:1)开启分页,跳转到内核代码
reset_early_page_tables()early_top_pgt 清空无直接映射防止非法访问
切换后init_top_pgt高端内核映射(-2GB)正式内存管理,隔离用户空间

安全设计分析

  1. 编译时检查:通过 BUILD_BUG_ON 确保内存布局符合预期,避免运行时崩溃。
  2. 页表隔离:清除临时映射,防止利用物理地址攻击内核。
  3. 加密与消毒:SME和KASAN分别在硬件和软件层增强内存安全。
  4. 微码更新:及时修复CPU漏洞,提升系统安全性。

总结

x86_64_start_kernel 是 x86_64 架构内核启动的核心枢纽,负责:

  • 验证内存布局:确保编译时地址分配合理性。
  • 初始化关键子系统:内存加密、错误检测、中断管理。
  • 环境切换:从临时页表切换到正式内存管理。
  • 数据准备:传递启动参数,加载CPU微码。

该函数体现了 Linux 内核安全至上渐进初始化的设计哲学,为后续 start_kernel() 的执行奠定坚实基础。

x86_64_start_reservations

void __init __noreturn x86_64_start_reservations(char *real_mode_data)
{
	/* version is always not zero if it is copied */
	if (!boot_params.hdr.version)
		copy_bootdata(__va(real_mode_data));

	x86_early_init_platform_quirks();

	switch (boot_params.hdr.hardware_subarch) {
	case X86_SUBARCH_INTEL_MID:
		x86_intel_mid_early_setup();
		break;
	default:
		break;
	}

	start_kernel();
}

以下是对 x86_64_start_reservations 函数的逐行分析解释,结合 Linux 内核启动流程和 x86_64 架构特性:


函数定义

void __init __noreturn x86_64_start_reservations(char *real_mode_data)
  • __init: 标记函数仅在初始化阶段存在,完成后内存会被释放。
  • __noreturn: 函数不会返回(最终调用 start_kernel() 进入无限循环)。
  • real_mode_data: 指向实模式阶段传递的数据(如 boot_params)。

1. 复制启动数据(条件检查)

if (!boot_params.hdr.version)
    copy_bootdata(__va(real_mode_data));
  • 作用:确保 boot_params(启动参数结构体)已被正确初始化。
  • 逻辑分解
    • boot_params.hdr.version: 引导协议版本号,若为0表示数据未复制。
    • __va(real_mode_data): 将实模式数据的物理地址转换为内核虚拟地址。
    • copy_bootdata(): 将 real_mode_data 中的数据复制到 boot_params
  • 设计意图:避免重复复制(例如在多次调用时),确保数据完整性。

2. 平台早期特性初始化

x86_early_init_platform_quirks();
  • 作用:处理 x86 平台特定的早期硬件兼容性问题或配置。
  • 典型操作
    • 修复特定芯片组的 ACPI 或 IRQ 路由问题。
    • 启用/禁用某些 CPU 特性(如 TSX)。
    • 配置早期控制寄存器(如 CR0/CR4)。

3. 硬件子架构初始化

switch (boot_params.hdr.hardware_subarch) {
case X86_SUBARCH_INTEL_MID:
    x86_intel_mid_early_setup();
    break;
default:
    break;
}
  • hardware_subarch: 标识硬件子架构类型(如 Intel MID、CE4100 等)。
  • X86_SUBARCH_INTEL_MID: Intel 移动设备平台(如 Medfield、Cloverview)。
  • x86_intel_mid_early_setup(): 执行 Intel MID 特有的初始化:
    • 配置 IOAPIC 和定时器。
    • 初始化特定外设(如 SPI、GPIO)。
    • 设置内存映射区域。
  • 默认分支:其他子架构无需额外操作。

4. 进入内核主初始化

start_kernel();
  • 作用:调用通用内核入口函数,完成全局初始化。
  • 关键操作
    • 初始化调度器、内存管理、中断系统。
    • 挂载根文件系统。
    • 启动用户空间(init 进程)。
  • 永不返回:内核进入无限循环,处理中断和进程调度。

代码执行流程

x86_64_start_kernel()
    → x86_64_start_reservations()
        ├─ 复制 boot_params(若未初始化)
        ├─ 处理平台特性
        ├─ 配置子架构硬件
        └─ start_kernel()
            → rest_init()
                → kernel_init()
                    → 用户空间启动

内存与数据流

  1. 实模式数据
    Bootloader(如 GRUB)将硬件信息(内存布局、命令行参数)存储在 real_mode_data(物理地址),通过 copy_bootdata 转换为内核虚拟地址并填充到 boot_params

  2. 硬件子架构处理
    Intel MID 设备需要特殊映射(如保留区域),x86_intel_mid_early_setup() 确保内核适配其硬件限制。

  3. 平台特性修正
    x86_early_init_platform_quirks() 解决不同主板或芯片组的兼容性问题(如 Dell 系统特定行为)。


安全性与鲁棒性设计

  • 条件检查:通过 boot_params.hdr.version 避免重复复制导致数据覆盖。
  • 子架构隔离:仅针对特定硬件(如 Intel MID)执行代码,减少通用代码耦合。
  • 虚拟地址转换:使用 __va 确保在内核地址空间访问实模式数据。

典型场景示例

  • Intel MID 设备启动

    1. Bootloader 传递 hardware_subarch = X86_SUBARCH_INTEL_MID
    2. x86_intel_mid_early_setup() 初始化 SPI 控制器和触摸屏驱动。
    3. 内核适配 MID 内存布局(如保留视频缓冲区)。
  • 标准 x86_64 服务器启动

    1. hardware_subarch 保持默认值(非 MID)。
    2. 跳过子架构特定代码,直接进入 start_kernel()

总结

x86_64_start_reservations 是 x86_64 内核启动的关键过渡函数,职责包括:

  1. 数据完整性保障:确保启动参数正确传递。
  2. 硬件适配:处理平台和子架构的特定需求。
  3. 流程交接:最终将控制权移交通用内核入口 start_kernel()

该函数体现了 Linux 内核模块化硬件兼容性设计,为不同 x86 设备提供统一且灵活的启动路径。

物理内存(e820)

在伙伴管理系统正式工作之前,需要一个临时的内存分配机制来满足这个阶段的内存分配需求。最早的临时分配机制是bootmem,现在普遍使用的是memblock。

start_kernel----->setup_arch---->e820__memory_setup
/*
 * Calls e820__memory_setup_default() in essence to pick up the firmware/bootloader
 * E820 map - with an optional platform quirk available for virtual platforms
 * to override this method of boot environment processing:
 */
void __init e820__memory_setup(void)
{
	char *who;

	/* This is a firmware interface ABI - make sure we don't break it: */
	BUILD_BUG_ON(sizeof(struct boot_e820_entry) != 20);

	who = x86_init.resources.memory_setup();

	memcpy(e820_table_kexec, e820_table, sizeof(*e820_table_kexec));
	memcpy(e820_table_firmware, e820_table, sizeof(*e820_table_firmware));

	pr_info("BIOS-provided physical RAM map:\n");
	e820__print_table(who);
}

以下是对 e820__memblock_setup 函数的逐行分析,结合 Linux 内核的内存管理机制和 x86 架构特性:


函数功能

该函数的作用是 将 BIOS 或 UEFI 提供的 E820 内存布局信息,转换为内核早期内存管理器 memblock 的可用/保留内存区域,完成物理内存的初始映射。


代码逐行解析

1. 允许 memblock 动态扩容
memblock_allow_resize();
  • 背景memblock 初始化时默认分配固定大小的内存区域数组(INIT_MEMBLOCK_REGIONS = 128)。
  • 作用:启用动态调整 memblock.memorymemblock.reserved 数组的大小。
  • 必要性:当 E820 条目超过 128 时(如某些 EFI 系统),避免溢出导致数据丢失。

2. 遍历 E820 内存表
for (i = 0; i < e820_table->nr_entries; i++) {
    struct e820_entry *entry = &e820_table->entries[i];
    end = entry->addr + entry->size;
    if (end != (resource_size_t)end)
        continue;
  • e820_table:存储从 BIOS/UEFI 获取的内存布局信息,每个条目描述一个内存区域。
  • end 计算与校验:检查 addr + size 是否溢出 resource_size_t(通常是 u64)。若溢出则跳过该条目(通常因硬件错误或固件缺陷导致)。

3. 处理 SOFT_RESERVED 类型区域
    if (entry->type == E820_TYPE_SOFT_RESERVED)
        memblock_reserve(entry->addr, entry->size);
  • E820_TYPE_SOFT_RESERVED:由内核或引导加载器(如 GRUB)标记的软保留区域。
  • 操作:调用 memblock_reserve() 将其标记为保留,防止被意外分配。
  • 典型场景:KASLR 的随机化区域、内核命令行参数占用的内存。

4. 过滤并添加可用内存
    if (entry->type != E820_TYPE_RAM && entry->type != E820_TYPE_RESERVED_KERN)
        continue;
    memblock_add(entry->addr, entry->size);
  • 条件:仅处理以下类型的内存区域:
    • E820_TYPE_RAM:可用物理内存。
    • E820_TYPE_RESERVED_KERN:内核保留区域(如内核代码、数据段)。
  • 操作:调用 memblock_add() 将区域添加到 memblock.memory,标记为可用。

5. 内存对齐修剪
memblock_trim_memory(PAGE_SIZE);
  • 作用:将所有内存区域的起始和结束地址按 PAGE_SIZE(通常 4KB)对齐。
  • 必要性
    • 确保后续页表映射不会出现部分页。
    • 避免内存分配器处理未对齐块带来的复杂性。

6. 调试输出
memblock_dump_all();
  • 作用:打印 memblock 的当前状态(memoryreserved 区域)。
  • 输出示例
    MEMBLOCK configuration:
     memory size = 0x1fff0000 reserved size = 0x1eef000
     memory.cnt  = 0x3
     memory[0x0]    [0x0000000000001000-0x000000000009ffff], 0x9f000 bytes
     memory[0x1]    [0x0000000000100000-0x000000001ffeffff], 0x1fe00000 bytes
     reserved.cnt  = 0x3
     reserved[0x0]  [0x0000000000000000-0x0000000000000fff], 0x1000 bytes
    

关键数据结构

E820 条目类型
类型描述
E820_TYPE_RAM1可用物理内存
E820_TYPE_RESERVED2保留区域(硬件/固件使用)
E820_TYPE_ACPI3ACPI 表格区域
E820_TYPE_NVS4ACPI NVS 内存
E820_TYPE_UNUSABLE5不可用内存
E820_TYPE_PMEM7持久性内存
E820_TYPE_RESERVED_KERN128内核保留区域
E820_TYPE_SOFT_RESERVED129内核软保留区域

内存管理流程

  1. BIOS/UEFI 阶段:固件检测内存布局,生成 E820 表。
  2. 内核启动早期e820__memblock_setup 将 E820 数据转换为 memblock 区域。
  3. memblock 阶段:内核通过 memblock 管理内存分配/保留。
  4. 伙伴系统初始化memblock 数据最终迁移到伙伴系统,完成内存管理切换。

设计要点

  • 兼容性:支持不同固件(BIOS/UEFI)的 E820 表格式。
  • 安全性:严格过滤不可用区域(如 E820_TYPE_UNUSABLE),防止分配到危险内存。
  • 灵活性:动态扩容机制应对复杂内存布局。
  • 性能:早期按页对齐减少后续管理开销。

典型场景示例

  • 系统启动时
    BIOS 报告内存中存在一个 E820_TYPE_RESERVED 区域(如 APIC 寄存器空间),该区域不会被添加到 memblock.memory,确保内核不会分配此区域。

  • KASLR 启用时
    引导加载器将随机化的内核代码区域标记为 E820_TYPE_SOFT_RESERVEDmemblock_reserve() 保护该区域不被其他组件占用。


总结

e820__memblock_setup 是 x86 架构内核启动的关键步骤,其核心任务是将原始内存信息转换为内核可管理的结构,为后续内存初始化奠定基础。通过精确处理不同类型的内存区域,确保内核稳定性和安全性。

在setup_arch()后续过程中,可以使用memblock来分配和释放内存

memblock已经有内存可以分配了,可以通过memblock_alloc()来分配物理内存:

static __always_inline void *memblock_alloc(phys_addr_t size, phys_addr_t align)
{
	return memblock_alloc_try_nid(size, align, MEMBLOCK_LOW_LIMIT,
				      MEMBLOCK_ALLOC_ACCESSIBLE, NUMA_NO_NODE);
}

在buddy系统建立好后,释放memblock中所有的内存到buddy中,有buddy来承担后续的内存分配工作:

start_kernel()mm_init()mem_init():

(更详细可以参考参考链接:https://zhuanlan.zhihu.com/p/613004422)


http://www.kler.cn/a/598160.html

相关文章:

  • 应用程序安全趋势:左移安全、人工智能和开源恶意软件
  • 游戏引擎学习第176天
  • 修改服务器windows远程桌面默认端口号
  • 2025.03.21首板涨停股票分析
  • 机器学习-聚类模型
  • 一加13T手机三证齐全:骁龙8至尊版小屏机、80W快充
  • 5G智慧工厂专网部署:IPLOOK助力制造业数字化转型
  • 第二届图像处理与人工智能国际学术会议(ICIPAI2025)
  • setenv ethaddr b8:ae:1d:01:00:00失效错误怎么解决❌
  • Python环境安装
  • 2025年03月18日柯莱特(外包宁德)一面前端面试
  • Spring Boot 整合 RabbitMQ:注解声明队列与交换机详解
  • 【Go】基本数据类型
  • Jenkins 配置python项目和allure
  • 蓝桥杯备考-》单词接龙
  • Linux shell脚本3-if语句、case语句、for语句、while语句、until语句、break语句、continue语句,格式说明及程序验证
  • 苹果上架APP遇到提示缺少出口合规证明时应该如何处理-什么是APP加密文稿-优雅草卓伊凡
  • (每日一道算法题)翻转对
  • MySQL 锁机制详解
  • 2.企业级AD活动目录架构与设计原则实战指南