Linux内核,内存分布
x86_64的物理地址范围为64bit,但是因为地址空间太大目前不可能完全用完,当前支持57bit和48bit两种虚拟地址模式。
地址模式 | 单个空间 | 用户地址空间 | 内核地址空间 |
---|---|---|---|
32位 | 2G | 0x00000000 - 0x7FFFFFFF | 0x80000000 - 0xFFFFFFFF |
64位(48bit) | 128T | 0x00000000 00000000 - 0x00007FFF FFFFFFFF | 0xFFFF8000 00000000 - 0xFFFFFFFF FFFFFFFF |
64位(57bit) | 64P | 0x00000000 00000000 - 0x00FFFFFF FFFFFFFF | 0xFF000000 00000000 - 0xFFFFFFFF FFFFFFFF |
48bit模式的地址空间布局(4级页表)
Start addr | Offset | End addr | Size | VM area description | 描述 |
---|---|---|---|---|---|
0000000000000000 | 0 | 00007fffffffffff | 128 TB | user-space virtual memory, different per mm | 用户地址空间,每个进程mm指向的都不同 |
0000800000000000 | +128 TB | ffff7fffffffffff | ~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 TB | ffff87ffffffffff | 8 TB | … guard hole, also reserved for hypervisor | |
ffff880000000000 | -120 TB | ffff887fffffffff | 0.5 TB | LDT remap for PTI | LDT(Local Descriptor Table):局部描述符表KPTI(Kernel page-table isolation):内核页表隔离 |
ffff888000000000 | -119.5 TB | ffffc87fffffffff | 64 TB | direct mapping of all physical memory (page_offset_base) | 线性映射的区域 |
0000000000000000 | -55.5 TB | ffffc8ffffffffff | 0.5 TB | … unused hole | |
ffffc90000000000 | -55 TB | ffffe8ffffffffff | 32 TB | vmalloc/ioremap space (vmalloc_base) | vmalloc和ioremap空间 |
ffffe90000000000 | -23 TB | ffffe9ffffffffff | 1 TB | … unused hole | |
ffffea0000000000 | -22 TB | ffffeaffffffffff | 1 TB | virtual memory map (vmemmap_base) | page结构存储的位置 |
ffffeb0000000000 | -21 TB | ffffebffffffffff | 1 TB | … unused hole | |
ffffec0000000000 | -20 TB | fffffbffffffffff | 16 TB | KASAN shadow memory | KASAN影子内存 |
Identical layout to the 56-bit one from here on: | 从这里开始,与56-bit布局相同: | ||||
fffffc0000000000 | 0 | 00007fffffffffff | 128 TB | user-space virtual memory, different per mm | 用户地址空间,每个进程mm指向的都不同 |
0000000000000000 | -4 TB | fffffdffffffffff | 2 TB | … unused hole | |
vaddr_end for KASLR | |||||
fffffe0000000000 | -2 TB | fffffe7fffffffff | 0.5 TB | cpu_entry_area mapping | |
fffffe8000000000 | -1.5 TB | fffffeffffffffff | 0.5 TB | … unused hole | |
ffffff0000000000 | -1 TB | ffffff7fffffffff | 0.5 TB | %esp fixup stacks | |
ffffff8000000000 | -512 GB | ffffffeeffffffff | 444 GB | … unused hole | |
ffffffef00000000 | -68 GB | fffffffeffffffff | 64 GB | EFI region mapping space | |
ffffffff00000000 | -4 GB | ffffffff7fffffff | 2 GB | … unused hole | |
ffffffff80000000 | -2 GB | ffffffff9fffffff | 512 MB | kernel text mapping, mapped to physical address 0 | 内核代码区域 |
ffffffff80000000 | -2048 MB | ||||
ffffffffa0000000 | -1536 MB | fffffffffeffffff | 1520 MB | module mapping space | 模块加载区域 |
ffffffffff000000 | -16 MB | ||||
FIXADDR_START | ~-11 MB | ffffffffff5fffff | ~0.5 MB | kernel-internal fixmap range, variable size and offset | |
ffffffffff600000 | -10 MB | ffffffffff600fff | 4 kB | legacy vsyscall ABI | |
fffffffffffe00000 | -2 MB | fffffffffffffffff | 2 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块区域的映射:
region | size | desctipt |
---|---|---|
identity mapping | 1G | 虚拟地址和物理地址相等 |
direct mapping | 1G | 线性映射空间,起始虚拟地址 |
kernel image | 512M | 内核映像映射空间 |
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) | 正式内存管理,隔离用户空间 |
安全设计分析
- 编译时检查:通过
BUILD_BUG_ON
确保内存布局符合预期,避免运行时崩溃。 - 页表隔离:清除临时映射,防止利用物理地址攻击内核。
- 加密与消毒:SME和KASAN分别在硬件和软件层增强内存安全。
- 微码更新:及时修复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()
→ 用户空间启动
内存与数据流
-
实模式数据:
Bootloader(如 GRUB)将硬件信息(内存布局、命令行参数)存储在real_mode_data
(物理地址),通过copy_bootdata
转换为内核虚拟地址并填充到boot_params
。 -
硬件子架构处理:
Intel MID 设备需要特殊映射(如保留区域),x86_intel_mid_early_setup()
确保内核适配其硬件限制。 -
平台特性修正:
x86_early_init_platform_quirks()
解决不同主板或芯片组的兼容性问题(如 Dell 系统特定行为)。
安全性与鲁棒性设计
- 条件检查:通过
boot_params.hdr.version
避免重复复制导致数据覆盖。 - 子架构隔离:仅针对特定硬件(如 Intel MID)执行代码,减少通用代码耦合。
- 虚拟地址转换:使用
__va
确保在内核地址空间访问实模式数据。
典型场景示例
-
Intel MID 设备启动:
- Bootloader 传递
hardware_subarch = X86_SUBARCH_INTEL_MID
。 x86_intel_mid_early_setup()
初始化 SPI 控制器和触摸屏驱动。- 内核适配 MID 内存布局(如保留视频缓冲区)。
- Bootloader 传递
-
标准 x86_64 服务器启动:
hardware_subarch
保持默认值(非 MID)。- 跳过子架构特定代码,直接进入
start_kernel()
。
总结
x86_64_start_reservations
是 x86_64 内核启动的关键过渡函数,职责包括:
- 数据完整性保障:确保启动参数正确传递。
- 硬件适配:处理平台和子架构的特定需求。
- 流程交接:最终将控制权移交通用内核入口
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.memory
和memblock.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
的当前状态(memory
和reserved
区域)。 - 输出示例:
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_RAM | 1 | 可用物理内存 |
E820_TYPE_RESERVED | 2 | 保留区域(硬件/固件使用) |
E820_TYPE_ACPI | 3 | ACPI 表格区域 |
E820_TYPE_NVS | 4 | ACPI NVS 内存 |
E820_TYPE_UNUSABLE | 5 | 不可用内存 |
E820_TYPE_PMEM | 7 | 持久性内存 |
E820_TYPE_RESERVED_KERN | 128 | 内核保留区域 |
E820_TYPE_SOFT_RESERVED | 129 | 内核软保留区域 |
内存管理流程
- BIOS/UEFI 阶段:固件检测内存布局,生成 E820 表。
- 内核启动早期:
e820__memblock_setup
将 E820 数据转换为memblock
区域。 - memblock 阶段:内核通过
memblock
管理内存分配/保留。 - 伙伴系统初始化:
memblock
数据最终迁移到伙伴系统,完成内存管理切换。
设计要点
- 兼容性:支持不同固件(BIOS/UEFI)的 E820 表格式。
- 安全性:严格过滤不可用区域(如
E820_TYPE_UNUSABLE
),防止分配到危险内存。 - 灵活性:动态扩容机制应对复杂内存布局。
- 性能:早期按页对齐减少后续管理开销。
典型场景示例
-
系统启动时:
BIOS 报告内存中存在一个E820_TYPE_RESERVED
区域(如 APIC 寄存器空间),该区域不会被添加到memblock.memory
,确保内核不会分配此区域。 -
KASLR 启用时:
引导加载器将随机化的内核代码区域标记为E820_TYPE_SOFT_RESERVED
,memblock_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)