2. cmdline 传递机制:Bootloader 如何将 cmdline 传递给内核?

Bootloader(如 U-Boot、GRUB)负责加载内核映像并跳转到内核入口。在跳转之前,Bootloader 必须将内核启动参数(cmdline)传递给内核。传递机制经历了从 ATAG(旧式)到 设备树(DTB)chosen 节点(新式)的演进。理解这两种机制是分析内核启动参数来源的基础。

2.1 传递机制演进概览

特性 ATAG 机制(Legacy) DTB chosen 节点(Current)
适用架构 ARM 32-bit(早期) ARM64、RISC-V、x86(现代)
数据结构 链表结构,固定标签 扁平设备树(FDT),键值对
扩展性 差,新增参数需修改内核 好,通过修改 DTS 即可
cmdline 位置 ATAG_CMDLINE 标签 /chosen/bootargs 属性
当前状态 已废弃(ARM 32 仍兼容) 标准推荐方式
关键理解: 无论使用哪种机制,最终内核都会将 cmdline 字符串解析并存储到全局变量 saved_command_line 中,供后续模块使用。

2.2 ATAG 传递机制(Legacy)

ATAG(ARM Tags)是 ARM 32 位架构早期使用的参数传递协议。Bootloader 在内存中构建一个 ATAG 链表,内核启动时通过寄存器 r2 获取链表首地址。

2.2.1 ATAG 链表结构

每个 ATAG 节点由 tag_header 和 tag_body 组成,链表以 ATAG_NONE 结束。

struct tag_header {
    u32 size;   // 整个 tag 的大小(以字为单位)
    u32 tag;    // 标签类型(如 ATAG_CMDLINE = 0x54410009)
};

struct tag {
    struct tag_header hdr;
    union {
        struct tag_cmdline cmdline; // 存放 cmdline 字符串
        // ... 其他 tag 类型
    } u;
};

// 典型 cmdline tag 定义
struct tag_cmdline {
    char cmdline[1]; // 实际为可变长字符串
};

2.2.2 U-Boot 传递 cmdline 示例(ATAG 方式)

// U-Boot 环境变量设置
setenv bootargs 'console=ttyS0,115200 root=/dev/mmcblk0p2 rw'

// U-Boot 启动命令(ATAG 方式)
bootm 0x80000000 0x81000000 0x82000000
// 参数含义:内核地址、ramdisk地址、ATAG链表地址

U-Boot 内部会将 bootargs 环境变量解析并填充到 ATAG 链表的 ATAG_CMDLINE 标签中,然后将链表首地址通过寄存器 r2 传递给内核。

ATAG 传递流程:
  1. U-Boot 读取 bootargs 环境变量字符串。
  2. U-Boot 在内存中构建 ATAG 链表,添加 ATAG_CORE、ATAG_MEM、ATAG_CMDLINE 等标签。
  3. U-Boot 跳转到内核入口时,设置 r2 = ATAG 链表首地址。
  4. 内核启动汇编代码读取 r2,调用 parse_tags() 解析链表。
  5. 遇到 ATAG_CMDLINE 标签,将字符串复制到 default_command_line。

2.3 设备树(DTB)chosen 节点传递机制

现代内核(尤其是 ARM64、RISC-V)使用设备树(Device Tree)来描述硬件。Bootloader 将 cmdline 放入 DTB 的 /chosen 节点的 bootargs 属性中,内核启动时从 DTB 解析。

2.3.1 chosen 节点结构

/dts-v1/;

/ {
    model = "Example Board";
    compatible = "vendor,example-board";

    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rw init=/sbin/init";
        stdout-path = &uart0;
    };

    // ... 其他设备节点
};

关键属性说明:

  • bootargs:内核启动参数字符串,与 cmdline 格式完全一致。
  • stdout-path:指定控制台设备路径,内核早期启动时使用。
  • linux,initrd-start / linux,initrd-end:可选,指定 initramfs 内存地址范围。

2.3.2 U-Boot 传递 cmdline 示例(DTB 方式)

// U-Boot 环境变量设置
setenv bootargs 'console=ttyS0,115200 root=/dev/mmcblk0p2 rw'
setenv fdt_addr 0x88000000

// U-Boot 启动命令(DTB 方式)
bootm 0x80000000 - 0x88000000
// 参数含义:内核地址、无ramdisk、DTB地址

// 或者手动修改 DTB 中的 bootargs
fdt addr 0x88000000
fdt set /chosen bootargs "${bootargs}"
bootm 0x80000000 - 0x88000000
注意事项: 如果 DTB 中 /chosen 节点不存在 bootargs 属性,内核将使用编译时默认的 cmdline(通过 CONFIG_CMDLINE 配置)。U-Boot 的 fdt set 命令可以动态注入或覆盖 bootargs。

2.4 内核解析流程对比

内核在启动早期(setup_arch() 函数中)根据架构选择解析方式:

架构 解析函数 数据来源
ARM 32 setup_machine_tags() ATAG 链表或 DTB(兼容)
ARM64 setup_machine_fdt() DTB chosen 节点
RISC-V setup_bootmem() DTB chosen 节点
x86 parse_setup_data() boot_params + DTB(可选)

2.5 传递机制流程图

下图展示了从 Bootloader 到内核的完整 cmdline 传递路径:

Bootloader 到内核的 cmdline 传递机制 Bootloader 阶段 U-Boot / GRUB 读取 bootargs 环境变量 传递方式选择 根据架构/配置 决定 ATAG 或 DTB ATAG 方式(Legacy) 构建 ATAG 链表 ATAG_CMDLINE 标签 r2 寄存器传递地址 DTB 方式(Current) 加载/修改 DTB /chosen/bootargs 属性 x0/x1 寄存器传递 DTB 地址 内核解析阶段 setup_arch() → parse_tags() 或 early_init_dt_scan_chosen() → saved_command_line 说明 ARM32 兼容两种方式 ARM64/RISC-V 仅 DTB x86 使用 boot_params

2.6 实际调试技巧

调试建议:
  • 查看实际传递的 cmdline: 内核启动后执行 cat /proc/cmdline 查看最终生效的启动参数。
  • U-Boot 下验证 DTB 内容: 使用 fdt list /chosen 查看当前 DTB 中 chosen 节点的所有属性。
  • 内核编译时默认 cmdline: 配置 CONFIG_CMDLINE 作为后备参数,当 Bootloader 未传递时使用。
  • 追加 vs 覆盖: 内核支持 CONFIG_CMDLINE_FROM_BOOTLOADER(覆盖)和 CONFIG_CMDLINE_EXTEND(追加)两种策略。

2.7 总结

cmdline 传递机制的核心是 Bootloader 与内核之间的数据契约:

  • ATAG 方式:通过内存链表传递,已逐渐被淘汰,但 ARM 32 仍保留兼容。
  • DTB chosen 节点:现代标准方式,通过 bootargs 属性传递,灵活且可扩展。
  • 内核统一处理:无论哪种方式,最终都会将 cmdline 字符串存入 saved_command_line,供后续解析使用。

理解这一机制有助于排查启动参数未生效、参数被覆盖、以及多平台移植时的 cmdline 配置问题。