3. 内核早期解析流程:从 head.S 到 start_kernel,setup_arch 如何读取 cmdline
内核启动参数(cmdline)的传递与解析,是系统初始化过程中最早的关键数据流之一。从汇编入口 head.S 到 C 语言入口 start_kernel,再到架构相关的 setup_arch,cmdline 经历了“携带 → 保存 → 解析 → 使用”四个阶段。本章将逐层拆解这一流程,并重点说明 setup_arch 如何从 bootloader 传递的数据中提取出最终的 cmdline 字符串。
3.1 整体数据流:从 Bootloader 到内核
cmdline 的传递路径可以概括为以下五个关键节点:
3.2 head.S 阶段:汇编层的 cmdline 保存
在 head.S 中,内核尚未建立完整的 C 运行环境。此时 cmdline 的传递完全依赖寄存器或固定的内存位置。不同架构的实现略有差异,但核心逻辑一致:
| 架构 | 入口文件 | cmdline 传递方式 | 关键寄存器/变量 |
|---|---|---|---|
| ARM64 | arch/arm64/kernel/head.S | DTB 地址保存在 x0,cmdline 从 DTB chosen 节点读取 | x0 (DTB 地址) |
| x86_64 | arch/x86/boot/compressed/head_64.S | boot_params 结构体中的 cmdline_ptr 字段 | %rsi (boot_params 地址) |
| RISC-V | arch/riscv/kernel/head.S | a0 寄存器传递 DTB 地址 | a0 (DTB 地址) |
__primary_switch,其中会保存 DTB 地址到 __fdt_pointer 全局变量。这个变量在后续的 setup_arch 中被读取,用于提取 cmdline。
3.3 start_kernel:C 语言入口的 cmdline 初始化
start_kernel 是内核启动的第一个 C 语言函数。在它执行之前,cmdline 已经以原始字符串的形式存在于内存中。start_kernel 通过调用 setup_arch 来完成架构相关的初始化,其中就包括 cmdline 的正式解析和存储。
关键代码路径如下:
// init/main.c
asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
{
char *command_line;
// ... 早期初始化 ...
setup_arch(&command_line); // 架构相关初始化,传入 cmdline 指针
// ... 后续初始化 ...
}
注意 setup_arch 的参数 &command_line 是一个指向指针的指针。在 setup_arch 内部,会将 cmdline 的地址赋值给这个参数,从而让 start_kernel 获得最终的 cmdline 字符串。
3.4 setup_arch:架构相关的 cmdline 读取
setup_arch 是每个架构必须实现的函数。它负责从 bootloader 传递的数据结构中提取 cmdline,并将其存储到全局数组 boot_command_line 中。以 ARM64 为例,其实现位于 arch/arm64/kernel/setup.c:
// arch/arm64/kernel/setup.c
void __init setup_arch(char **cmdline_p)
{
// 1. 从 DTB 中提取 cmdline
// early_fdt_ptr 指向 head.S 保存的 DTB 地址
// early_init_dt_scan_chosen 会解析 DTB 的 chosen 节点
// 并将 bootargs 属性复制到 boot_command_line 数组
early_init_dt_scan_chosen(boot_command_line);
// 2. 如果 DTB 中没有 cmdline,则使用默认值
if (!boot_command_line[0])
strlcpy(boot_command_line, CONFIG_CMDLINE, COMMAND_LINE_SIZE);
// 3. 将 cmdline 指针返回给 start_kernel
*cmdline_p = boot_command_line;
// ... 其他架构初始化 ...
}
3.5 关键数据结构:boot_command_line 与 COMMAND_LINE_SIZE
boot_command_line 是一个全局字符数组,定义在 init/main.c 中:
// init/main.c
#define COMMAND_LINE_SIZE 2048
static char __initdata boot_command_line[COMMAND_LINE_SIZE];
| 字段 | 说明 | 默认大小 |
|---|---|---|
boot_command_line |
存储最终 cmdline 的全局数组 | 2048 字节 |
COMMAND_LINE_SIZE |
cmdline 最大长度,可配置 | 2048 (x86/ARM64 默认) |
CONFIG_CMDLINE |
内核编译时内置的默认 cmdline | 由 .config 决定 |
CONFIG_CMDLINE 为空,则 boot_command_line 将是一个空字符串。这会导致内核启动时没有任何启动参数,可能引发设备驱动初始化失败。
3.6 调试开关:earlyprintk 与 cmdline 早期打印
在 setup_arch 执行期间,常规的 printk 可能尚未就绪。为了调试 cmdline 的读取是否正确,内核提供了 earlyprintk 机制:
- earlyprintk:在控制台初始化之前,通过架构特定的早期串口输出调试信息。
- 使用方法:在 bootloader 的 cmdline 中添加
earlyprintk参数,例如earlyprintk=uart8250,io,0x3f8,115200。 - 典型输出:
Early console on uart8250 at 0x3f8,随后可以打印boot_command_line的内容。
在 setup_arch 中,可以手动添加 early printk 来验证 cmdline 是否被正确读取:
// 在 setup_arch 中添加调试代码
void __init setup_arch(char **cmdline_p)
{
early_init_dt_scan_chosen(boot_command_line);
// 调试:打印 cmdline 的前 128 个字符
early_printk("cmdline: %s\n", boot_command_line);
*cmdline_p = boot_command_line;
// ...
}
3.7 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 内核启动参数不生效 | DTB 中 chosen 节点缺失或 bootargs 为空 | 使用 fdtdump 检查 DTB 内容 |
| earlyprintk 无输出 | 串口地址或波特率配置错误 | 确认 bootloader 中串口初始化正确 |
| cmdline 被截断 | COMMAND_LINE_SIZE 过小 | 增大 COMMAND_LINE_SIZE 或缩短 cmdline |
| CONFIG_CMDLINE 覆盖了 DTB 的 cmdline | 内核配置了 CONFIG_CMDLINE_FORCE |
检查 .config 中的 CONFIG_CMDLINE_FORCE 选项 |
bootargs 和 CONFIG_CMDLINE 作为后备。使用 earlyprintk 在 setup_arch 中打印 cmdline,可以快速定位参数传递问题。
3.8 本章小结
cmdline 从 Bootloader 到 setup_arch 的传递路径,是内核启动早期最重要的数据流之一。理解这一流程,有助于排查启动参数不生效、设备初始化失败等问题。关键要点如下:
- head.S 负责保存 DTB 或 boot_params 的地址到寄存器或全局变量。
- start_kernel 调用
setup_arch,并传入 cmdline 指针的地址。 - setup_arch 通过
early_init_dt_scan_chosen从 DTB 中提取 cmdline,存储到boot_command_line。 - 如果 DTB 中没有 cmdline,则使用
CONFIG_CMDLINE作为后备。 - earlyprintk 是调试 cmdline 读取过程的有效工具。