Linux设备树4(基于Linux6.6)---内核head.S对uboot传参的处理
一、uboot传参启动
boot启动内核是通过传入三个参数来启动的,arch/arc/lib/bootm.c
/* Subcommand: GO */
static void boot_jump_linux(bootm_headers_t *images, int flag)
{
void (*kernel_entry)(int zero, int arch, uint params);
unsigned int r0, r2;
int fake = (flag & BOOTM_STATE_OS_FAKE_GO);
kernel_entry = (void (*)(int, int, uint))images->ep;
debug("## Transferring control to Linux (at address %08lx)...\n",
(ulong) kernel_entry);
bootstage_mark(BOOTSTAGE_ID_RUN_OS);
printf("\nStarting kernel ...%s\n\n", fake ?
"(fake run for tracing)" : "");
bootstage_mark_name(BOOTSTAGE_ID_BOOTM_HANDOFF, "start_kernel");
cleanup_before_linux();
if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len) {
r0 = 2;
r2 = (unsigned int)images->ft_addr;
} else {
r0 = 1;
r2 = (unsigned int)env_get("bootargs");
}
smp_set_core_boot_addr((unsigned long)kernel_entry, -1);
smp_kick_all_cpus();
if (!fake)
kernel_entry(r0, 0, r2);
}
kernel_entry为内核zImage在内存的首地址。
之前我们传的三个参数分别是:
0,芯片的机器ID,uboot给内核参数tag在内存的首地址r2。
查看上面代码,可以看到r2也可以是另设备树文件(dtb)在在内存的地址。
对于使用tag传参给内核,机器的ID是必须要和内核中配置的芯片一致的。
而对于使用设备树来启动内核,机器ID则不是必须的。
分析的linux6.6内核默认支持设备树传参启动。
二、uboot传参过程分析
2.1、启动入口
在head.S文件中,ARM64架构的启动入口通常是_start符号。这是内核被加载到内存中后的第一个执行点。
.globl _start
.type _start, %function
_start:
adr_l x0, __PHYS_OFFSET // 获取物理内存偏移(通常是内核加载的物理地址)
bl el3_setup // 跳转到EL3(Exception Level 3)设置代码
2.2、EL3 设置
el3_setup函数进行一些基本的EL3级别的设置,包括禁用中断、配置栈指针等。
el3_setup:
// 禁用中断,配置栈指针等
// ...
bl el2_setup // 跳转到EL2设置代码
2.3、EL2 设置
el2_setup函数进一步配置EL2级别的环境,并准备跳转到内核的C语言启动代码。
el2_setup:
// 配置EL2环境
// ...
adr_l x1, __dtb_start_addr // 获取设备树基地址(可能由U-Boot传递)
adr_l x2, __dtb_end_addr // 获取设备树结束地址
mov x3, #0 // 初始化参数计数器
// 检查U-Boot是否传递了设备树地址(通过ATAGS或FDT)
ldr x4, =__atags_ptr // ATAGS指针位置
ldr w5, [x4] // 读取ATAGS的第一个字(标识)
cmp w5, #ATAG_NONE // 检查是否为空(无ATAGS)
b.eq check_fdt // 如果是空,则检查FDT
// 处理ATAGS
process_atags:
// 解析ATAGS并保存到内核数据结构中
// ...
b done_parsing
// 检查FDT(设备树扁平化格式)
check_fdt:
ldr x6, =fdt_addr // FDT地址(可能由U-Boot设置)
ldr x7, [x6, #FDT_SIZE_OFFSET] // 读取FDT大小
cmp x7, #0
b.eq no_fdt_or_atags // 如果没有FDT,则报错
mov x1, x6 // 将FDT地址保存到x1
mov x2, x7 // 将FDT大小保存到x2
b done_parsing
// 如果没有ATAGS或FDT
no_fdt_or_atags:
// 错误处理
// ...
b hang
// 完成解析
done_parsing:
// 保存解析后的参数(如设备树地址)
// ...
adr_l x8, __primary_arch_data // 获取主架构数据指针
str x1, [x8, #__arch_data_fdt_addr] // 保存FDT地址
// 跳转到C语言启动代码
mov x0, #0
bl kernel_entry // 跳转到内核入口
2.4、参数解析
在上面的代码中,内核通过检查__atags_ptr和fdt_addr来确定U-Boot是否传递了ATAGS或FDT。
- ATAGS:旧的启动参数传递方式,通过一系列的标签传递各种信息。
- FDT:现代的启动参数传递方式,使用设备树扁平化格式(Flattened Device Tree)传递复杂的硬件配置信息。
2.5、跳转到C语言启动代码
一旦解析完启动参数,内核将跳转到C语言启动代码,通常是kernel_entry函数。这个函数会进一步初始化内核环境,包括内存管理、调度器、设备树解析等。
kernel_entry:
// C语言启动代码入口
// ...
2.6、start_kernel 函数的执行
start_kernel 函数是 Linux 内核的初始化入口,它位于 init/main.c 文件中。在内核启动的第一阶段,start_kernel 会处理与架构相关的初始化工作,诸如:
- 解析设备树
- 配置硬件
- 初始化内存管理
- 启动系统进程
以下是 start_kernel 函数的大致执行流程:
void __init start_kernel(void)
{
// 调用 setup_arch 函数进行架构相关的初始化
setup_arch(&command_line);
// 内存管理初始化
mm_init();
// 解析设备树(r1 中存储设备树地址)
if (early_init_dt_scan())
pr_err("DTB: Failed to initialize device tree");
// 启动内核进程
kernel_thread();
}
2.7、内核命令行参数处理
内核命令行参数存储在 r2 中,内核会通过 setup_arch 等函数对其进行解析。命令行参数用于定制内核的启动行为,例如指定根文件系统的路径、启用调试模式等。
例如,在 start_kernel 函数中,命令行参数会传递给 setup_arch,以便进行相应的配置:
setup_arch(&command_line); // 解析并配置内核架构相关设置
command_line 中存储了内核启动时的命令行参数,通常是 U-Boot 启动时传递的内容。
2.8、设备树的解析
设备树(Device Tree)描述了硬件平台的详细信息(如 CPU 类型、内存布局、外设等)。内核通过解析设备树来获取硬件信息。设备树的地址由 U-Boot 传递,并存储在 r1 中。
内核会在初始化过程中读取设备树,并根据设备树中的信息来配置硬件。early_init_dt_scan 函数就是用于扫描和初始化设备树的:
if (early_init_dt_scan())
pr_err("DTB: Failed to initialize device tree");
设备树的解析是内核硬件初始化的一部分,它允许 Linux 支持多种硬件平台,而不需要在内核源码中硬编码硬件信息。
三、head.S 处理 U-Boot 传参的流程图
+-------------------------------------------+
| U-Boot 启动阶段 |
| (加载内核、设备树、命令行参数等) |
+-------------------------------------------+
|
v
+-------------------------------------------+
| ARM64 内核启动:进入 head.S |
| (此时在 EL3,进行初始化设置) |
+-------------------------------------------+
|
v
+-------------------------------------------+
| 获取 U-Boot 传递的参数 (如设备树、 |
| 内核镜像地址、命令行参数等) |
+-------------------------------------------+
|
v
+-------------------------------------------+
| 获取传递的 `atags` 或设备树地址 |
| (根据 U-Boot 配置,可能是设备树或 ATAGs)|
+-------------------------------------------+
|
v
+-------------------------------------------+
| 配置 MMU (内存管理单元),启用虚拟地址 |
| 映射,设置 EL1 运行环境 |
+-------------------------------------------+
|
v
+-------------------------------------------+
| 初始化中断控制器 (GIC) |
+-------------------------------------------+
|
v
+-------------------------------------------+
| 跳转到内核 C 语言代码入口 `start_kernel` |
| (将控制权交给 C 语言内核初始化代码) |
+-------------------------------------------+
|
v
+-------------------------------------------+
| 继续进行内核初始化,启动用户进程 |
+-------------------------------------------+
具体步骤说明:
-
U-Boot 启动阶段:
- 在 U-Boot 中,内核镜像、设备树和启动参数被加载到内存中。U-Boot 会根据配置启动内核,并将相关参数传递给内核启动。
-
进入
head.S:- 内核的启动代码
head.S在 ARM64 系统中位于引导流程的最前端,通常执行一些硬件相关的初始化任务。此时,内核处于 EL3(最高的异常级别),执行最基础的系统设置。
- 内核的启动代码
-
获取 U-Boot 传递的参数:
head.S通过读取从 U-Boot 传递的启动参数,主要是通过atags(或者设备树)获取内核镜像的位置、设备树的地址、以及命令行参数等。不同的启动配置可能会采用不同的方式来传递这些参数(如设备树地址、ATAGs)。
-
配置 MMU 和虚拟地址映射:
- 由于 ARM64 使用虚拟内存机制,在内核启动时需要配置 MMU 来创建虚拟到物理地址的映射。这一过程通过
head.S完成,确保内核在 EL1 中能够正确地访问内存。
- 由于 ARM64 使用虚拟内存机制,在内核启动时需要配置 MMU 来创建虚拟到物理地址的映射。这一过程通过
-
初始化中断控制器:
head.S还会初始化 GIC(Generic Interrupt Controller),以便处理器能够正确地处理硬件中断。
-
跳转到 C 语言入口
start_kernel:- 完成上述初始化后,
head.S会将控制权交给内核的 C 语言入口函数start_kernel,这是内核的核心初始化逻辑开始执行的地方。start_kernel函数将继续执行内核初始化、调度器设置、内存管理等任务。
- 完成上述初始化后,
-
继续进行内核初始化,启动用户进程:
- 在
start_kernel中,内核会初始化更多的硬件、驱动和内核子系统,最终进入调度器,准备启动用户空间的进程。
- 在
1387

被折叠的 条评论
为什么被折叠?



