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 传递流程:
- U-Boot 读取
bootargs环境变量字符串。 - U-Boot 在内存中构建 ATAG 链表,添加
ATAG_CORE、ATAG_MEM、ATAG_CMDLINE等标签。 - U-Boot 跳转到内核入口时,设置
r2 = ATAG 链表首地址。 - 内核启动汇编代码读取
r2,调用parse_tags()解析链表。 - 遇到
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 传递路径:
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 配置问题。