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 的传递路径可以概括为以下五个关键节点:

关键路径: Bootloader (U-Boot/GRUB) → DTB / boot_params → head.S 保存 → start_kernel 调用 → setup_arch 解析并存储到 boot_command_line 数组。
cmdline 从 Bootloader 到内核的完整数据流 Bootloader DTB / boot_params head.S start_kernel setup_arch 说明:ARM64 通过 DTB 的 chosen 节点传递 cmdline;x86 通过 boot_params 中的 cmdline_ptr 传递。 head.S 将 cmdline 地址保存到 x0/x1 寄存器或全局变量中,供后续 C 代码使用。

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 地址)
提示: ARM64 的 head.S 在早期会调用 __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;

    // ... 其他架构初始化 ...
}
setup_arch 读取 cmdline 的详细流程 DTB 地址 (x0) early_init_dt_scan_chosen boot_command_line[] ? boot_command_line[0] == '\0' ? CONFIG_CMDLINE 是 使用 DTB 中的 bootargs 否 *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 决定
注意: 如果 DTB 或 boot_params 中没有提供 cmdline,且 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 选项
最佳实践: 在开发阶段,建议在 DTB 中同时保留 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 读取过程的有效工具。