慢慢欣赏arm64内核启动12 primary_entry之__create_page_tables代码第二部分

这段代码的目的

VA_BITS_MIN=48 VA_BITS_MIN 是内核虚拟地址空间的位数(例如 48 位)。所以定义的虚拟地址空间范围是 [0, 2^48),也就是[0, 0xFFFF_FFFF_FFFF]。

抛出问题

恒等映射要求 虚拟地址 == 物理地址。如果系统 RAM 的物理起始地址非常高,例如 0x1_0000_0000_0000 == 2^48,那么直接用 VA_BITS 定义的虚拟地址空间无法容纳整个 ID 映射区域。
这个[0, 2^48)虚拟地址空间无法直接映射到高达  0x1_0000_0000_0000 的物理地址,因为  0x1_0000_0000_0000 已经超出了 2^48 的范围。

这段代码就是解决上述的问题,探测出恒等映射结束部分的物理地址,与VA_BITS_MIN进行比较,如果前者 大于VA_BITS_MIN,则扩充恒等映射的虚拟地址范围。

总体逻辑

检测:通过计算__idmap_text_end地址的前导零,判断默认的虚拟地址空间是否足以覆盖所有需要恒等映射的物理地址。
决策:如果足够,跳过扩展流程。如果不够,继续执行。
准备:存储计算出的新T0SZ值,并确保缓存一致性。
扩展:根据当前是VA_BITS < 48还是VA_BITS == 48,采取两种不同的策略来调整页表结构:
对于小VA空间:需要增加一个额外的页表转换级别。调用create_table_entry创建新级别的页表。
对于48位VA空间:不需要增加级别,但需要扩展顶级页表(PGD)的条目数。计算出新的条目数并保存。
最终目的:动态地配置ID映射的页表结构,确保即使物理内存地址非常高,也能成功创建 虚拟地址 = 物理地址 的恒等映射,从而保证内核在开启MMU后能继续正确执行。这体现了Linux内核强大的可移植性,能够适应各种不同的硬件地址布局。

分析代码

	/*
	 * Create the identity mapping.
	 */
	adrp	x0, idmap_pg_dir
	adrp	x3, __idmap_text_start		// __pa(__idmap_text_start)

#ifdef CONFIG_ARM64_VA_BITS_52
	mrs_s	x6, SYS_ID_AA64MMFR2_EL1
	and	x6, x6, #(0xf << ID_AA64MMFR2_LVA_SHIFT)
	mov	x5, #52
	cbnz	x6, 1f
#endif
	mov	x5, #VA_BITS_MIN
1:
	adr_l	x6, vabits_actual
	str	x5, [x6]
	dmb	sy
	dc	ivac, x6		// Invalidate potentially stale cache line

	/*
	 * VA_BITS may be too small to allow for an ID mapping to be created
	 * that covers system RAM if that is located sufficiently high in the
	 * physical address space. So for the ID map, use an extended virtual
	 * range in that case, and configure an additional translation level
	 * if needed.
	 *
	 * Calculate the maximum allowed value for TCR_EL1.T0SZ so that the
	 * entire ID map region can be mapped. As T0SZ == (64 - #bits used),
	 * this number conveniently equals the number of leading zeroes in
	 * the physical address of __idmap_text_end.
	 */
	adrp	x5, __idmap_text_end
	clz	x5, x5
	cmp	x5, TCR_T0SZ(VA_BITS_MIN) // default T0SZ small en
<think>我们正在讨论ARM32架构下Linux 5.10内核启动函数调用流程。由于思维导图无法直接以图形形式展示,我将用文本层级结构描述,并尽量详细地列出关键函数调用流程。 注意:Linux内核启动流程非常复杂,不同版本和平台可能有细微差异。以下基于ARM32架构的Linux 5.10内核进行梳理。 --- ### ARM32 Linux 5.10 内核启动函数调用流程 ``` ARM32 Linux 5.10 启动流程 │ ├── Bootloader阶段(U-Boot等) │ ├── 初始化硬件(时钟、内存、串口等) │ ├── 加载内核映像(zImage或Image)到内存 │ ├── 加载设备树(DTB)到内存 │ └── 跳转到内核入口(通常为0x8000或指定地址) │ ├── 内核入口(arch/arm/kernel/head.S) │ ├── stext(入口点) │ │ ├── 设置SVC模式,禁用中断 │ │ ├── 检查处理器ID和机器类型 │ │ ├── 创建初始页表(__create_page_tables) │ │ ├── 启用MMU(__enable_mmu) │ │ └── 跳转到__mmap_switched │ │ │ └── __mmap_switched │ ├── 复制数据段、清除BSS │ ├── 设置初始堆栈 │ └── 跳转到start_kernel(C语言入口) │ ├── 内核通用初始化(init/main.c) │ └── start_kernel() │ ├── setup_arch(&command_line) [arch/arm/kernel/setup.c] │ │ ├── 解析设备树(early_init_dt_scan()) │ │ ├── 初始化内存(mdesc->init(),如devicemaps_init()) │ │ ├── 解析命令行参数(parse_early_param()) │ │ └── paging_init() [初始化页表] │ │ │ ├── trap_init() [初始化异常向量] │ ├── mm_init() [内存管理初始化] │ │ └── mem_init() [arch/arm/mm/init.c] │ │ │ ├── init_IRQ() [中断初始化,arch/arm/kernel/irq.c] │ │ └── 根据设备树初始化中断控制器 │ │ │ ├── time_init() [时钟初始化] │ │ └── 通过设备树初始化系统时钟 │ │ │ ├── console_init() [控制台初始化] │ ├── rest_init() [启动其他初始化线程] │ ├── rest_init() [init/main.c] │ ├── 创建kernel_init线程(PID=1) │ │ └── kernel_init() │ │ ├── kernel_init_freeable() │ │ │ ├── 挂载根文件系统(prepare_namespace()) │ │ │ └── 尝试执行用户空间init程序 │ │ │ ├── /sbin/init │ │ │ ├── /etc/init │ │ │ ├── /bin/init │ │ │ └── /bin/sh │ │ │ │ │ └── 若失败则进入panic │ │ │ ├── 创建kthreadd线程(PID=2) │ │ └── 管理内核线程 │ │ │ └── 调用schedule()启动调度器,并进入idle循环(cpu_startup_entry) │ └── 用户空间初始化 ├── init进程执行/sbin/init(如systemd或sysvinit) ├── 根据配置文件启动系统服务 └── 进入用户登录界面 ``` --- ### 关键函数详细说明 1. **`stext`** (arch/arm/kernel/head.S) - 这是内核的入口点,由Bootloader直接跳转执行。 - 主要任务:验证处理器和机器类型,创建初始页表,启用MMU,然后跳转到C语言代码。 2. **`__mmap_switched`** (arch/arm/kernel/head-common.S) - 在启用MMU后执行,完成运行环境设置: - 复制数据段(从ROM到RAM) - 清零BSS段 - 设置栈指针 - 跳转到`start_kernel` 3. **`start_kernel`** (init/main.c) - 内核的C语言入口点,初始化所有核心子系统: - `setup_arch`:架构相关初始化,包括设备树解析、内存映射设置等。 - `trap_init`:设置异常处理向量(如中断、系统调用)。 - `mm_init`:内存管理子系统初始化,包括伙伴系统。 - `init_IRQ`:中断控制器初始化(如GIC)。 - `time_init`:初始化系统时钟源。 - `console_init`:初始化控制台,此时可以打印消息。 - `rest_init`:创建第一个用户进程。 4. **`rest_init`** (init/main.c) - 创建内核线程`kernel_init`(即init进程,PID=1)和`kthreadd`(内核线程守护进程,PID=2)。 - 调用`schedule`启动调度器,然后进入idle循环。 5. **`kernel_init`** (init/main.c) - 挂载根文件系统(通过设备树或内核命令行参数指定)。 - 尝试执行用户空间的init程序,若失败则触发内核panic。 --- ### 设备树(DTB)在启动流程中的作用 在ARM32平台中,设备树在启动早期被解析: - Bootloader将DTB加载到内存,并将地址传递给内核。 - 在`setup_arch`中调用`setup_machine_fdt`解析设备树,获取硬件信息(如内存布局、中断控制器等)[^1]。 --- ### 函数调用流程图(简化版) 由于文本限制,以下为简化调用序列: ``` stext (汇编) → __create_page_tables → __enable_mmu → __mmap_switched → start_kernel → setup_arch → early_init_dt_scan [解析设备树] → paging_init [初始化页表] → init_IRQ [中断初始化] → time_init [时钟初始化] → rest_init → kernel_thread(kernel_init) [创建init进程] → kernel_thread(kthreadd) [创建内核线程] → cpu_startup_entry [进入idle循环] ``` --- §§相关问题§§ 1. 在ARM32架构中,`__enable_mmu`函数具体是如何启用MMU的? 2. Linux内核启动过程中,设备树(DTB)是如何被解析并获取硬件信息的? 3. 在`start_kernel`函数中,内存管理子系统(`mm_init`)初始化的具体步骤是什么? 4. ARM32平台下,中断控制器(如GIC)的初始化流程是怎样的? 5. 内核如何从`kernel_init`过渡到用户空间的init程序?</think>以下是ARM32架构下Linux 5.10内核启动过程的函数调用流程思维导图及详细解析,基于内核源码分析: ### ARM32 Linux 5.10内核启动函数调用流程 ```mermaid graph TD A[Bootloader] --> B[stext] B --> C[__primary_switched] C --> D[start_kernel] D --> E[setup_arch] E --> F[setup_machine_fdt] F --> G[early_init_dt_scan] D --> H[mm_init] D --> I[init_IRQ] D --> J[time_init] D --> K[console_init] D --> L[rest_init] L --> M[kernel_thread] M --> N[kernel_init] N --> O[kernel_init_freeable] O --> P[prepare_namespace] P --> Q[mount_root] N --> R[run_init_process] ``` ### 详细函数调用解析 #### 1. Bootloader阶段(硬件相关) - 加载内核镜像到内存地址`0x8000` - 传递设备树地址(DTB)到`r2`寄存器 - 跳转到内核入口点`stext` #### 2. 内核入口(arch/arm/kernel/head.S) ```c ENTRY(stext) THUMB( badr r9, 1f ) // 设置SVC模式 bl __vet_atags // 验证设备树 bl __create_page_tables // 创建初始页表 ldr r13, =__mmap_switched // 设置栈指针 b __enable_mmu // 启用MMU END_PROCF(stext) ``` #### 3. 核心初始化(init/main.c) **`start_kernel()`关键调用序列:** ```c asmlinkage __visible void __init start_kernel(void) { setup_arch(&command_line); // 架构初始化 mm_init(); // 内存管理初始化 init_IRQ(); // 中断控制器初始化 time_init(); // 时钟初始化 console_init(); // 控制台初始化 rest_init(); // 剩余初始化 } ``` #### 4. 架构特定初始化(arch/arm/kernel/setup.c) ```c void __init setup_arch(char **cmdline_p) { setup_machine_fdt(__atags_pointer); // 解析设备树 early_init_dt_scan_nodes(); // 扫描设备树节点 arm_memblock_init(); // 内存区域初始化 paging_init(); // 分页初始化 unflatten_device_tree(); // 展开设备树 } ``` #### 5. 用户空间过渡(init/main.c) ```c static noinline void __init_refok rest_init(void) { kernel_thread(kernel_init, NULL, CLONE_FS); // PID=1 kernel_thread(kthreadd, NULL, CLONE_FS); // PID=2 cpu_startup_entry(CPUHP_ONLINE); // 进入idle循环 } static int __ref kernel_init(void *unused) { kernel_init_freeable(); // 挂载根文件系统 run_init_process("/sbin/init"); // 执行init进程 } ``` ### 关键数据结构说明 1. **设备树解析**: - `early_init_dt_scan()`扫描设备树获取内存信息 - `of_scan_flat_dt()`解析设备树节点[^1] 2. **中断初始化**: ```c void __init init_IRQ(void) { irqchip_init(); // 初始化中断控制器 if (IS_ENABLED(CONFIG_OF) && !machine_desc->init_irq) irq_domain_init(); // 基于设备树初始化中断域 } ``` 3. **文件系统挂载**: ```c void __init prepare_namespace(void) { mount_root(); // 挂载根文件系统 } ``` ### 启动流程时间线 | 阶段 | 时间点 | 关键事件 | |------|--------|----------| | 硬件初始化 | 0ms | CPU复位,Bootloader运行 | | 内核解压 | 50ms | zImage解压到内存 | | MMU启用 | 100ms | `__enable_mmu`完成 | | 设备树解析 | 150ms | `unflatten_device_tree`完成 | | 中断初始化 | 200ms | GIC控制器初始化完成 | | 控制台就绪 | 250ms | 串口输出"Booting Linux..." | | 用户空间启动 | 500ms | `/sbin/init`执行 | > 注:时间点为典型值,实际取决于硬件性能 --- ### 相关问题 1. ARM32架构下MMU启用过程中`__create_page_tables`函数的具体实现是怎样的? 2. 在设备树解析阶段,Linux内核如何将DTB转换为`device_node`数据结构? 3. 内核启动过程中`init_IRQ`函数如何初始化GIC中断控制器? 4. 从`kernel_init`到执行`/sbin/init`的过渡过程中涉及哪些安全机制? 5. ARM32启动过程中如何通过`atags`传递内核参数?[^1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值