第4章:Bootloader与内核交互
说实话,Bootloader和内核之间的通信,是整个系统启动中最容易出bug的环节。我见过太多工程师花了两周调驱动,最后发现是参数传错了。这一章,咱们就把这个交互过程彻底讲透。
4.1 启动参数传递:内核怎么知道它该干什么?
你想想看,内核是个通用软件。它不知道你用的是哪款芯片,不知道内存多大,更不知道串口用哪个地址。这些信息,全靠Bootloader告诉它。
传递方式主要有两种:ATAG 和 设备树(DTB)。咱们先看老前辈ATAG。
4.2 ATAG方式:老派但经典
ATAG是ARM Linux早期用的参数传递方式。说白了,就是在内存里放一个链表,每个节点存一种信息。
结构大概长这样:
struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;
} u;
};
每个tag都有一个类型标识。内核从起始地址开始,挨个解析这些tag。比如 ATAG_MEM 告诉内核内存范围,ATAG_CMDLINE 传递启动参数。
核心流程:
- Bootloader在内存某地址写入tag链表
- 内核启动时,读取该地址
- 遍历链表,提取参数
- 根据参数初始化硬件
我在项目中遇到过一个问题:某次移植Linux 3.0到老平台,内核死活不认内存。查了两天,发现是ATAG_MEM的地址写错了。嗯,这种低级错误,排查起来最费时间。
4.3 设备树方式:现代方案
ATAG的问题很明显:硬编码、难扩展。每加一个新硬件,就得改内核代码。设备树(Device Tree)就是来解决这个痛点的。
设备树用文本描述硬件,编译成二进制DTB文件。Bootloader把DTB地址传给内核,内核自己解析。
一个简单的设备树片段:
/dts-v1/;
/ {
model = "TI AM335x BeagleBone";
compatible = "ti,am335x-bone", "ti,am33xx";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>; // 256MB
};
chosen {
bootargs = "console=ttyO0,115200 root=/dev/mmcblk0p2";
};
};
你看,内存大小、启动参数、外设信息,全在文本里。改硬件?改dts文件就行,不用动内核。
我个人习惯: 调试阶段先用ATAG快速验证,稳定后切到设备树。但新项目我建议直接上设备树,省得后面再迁移。
4.4 两种方式对比:ATAG vs 设备树
咱们用表格直观对比一下:
| 对比项 | ATAG | 设备树(DTB) |
|---|---|---|
| 数据结构 | 链表,顺序解析 | 树形结构,节点可嵌套 |
| 扩展性 | 差,加新tag需改内核 | 好,加新节点无需改内核 |
| 硬件描述能力 | 弱,只能传基本参数 | 强,可描述复杂外设 |
| 多平台支持 | 每个平台一套代码 | 一套内核,多个dtb |
| 调试难度 | 简单,直接看内存 | 稍复杂,需dtc工具 |
| 社区支持 | 已废弃(ARM64不支持) | 主流方案 |
为什么会这样?说白了,ATAG是ARM Linux早期的临时方案。那时候硬件简单,传几个参数就够了。现在SoC越来越复杂,一个芯片里集成了几十个外设,ATAG根本扛不住。
4.5 为什么设备树取代了ATAG?
我总结三个核心原因:
- 解耦硬件描述:设备树把硬件信息从内核源码里抽出来。换板子?换dtb就行,不用重新编译内核。我在一个项目里同时支持三款板子,就靠三份dtb文件。
- 标准化:设备树是开放标准,ARM、RISC-V、x86都在用。ATAG是ARM私有协议,其他架构不认。
- 可维护性:设备树支持include、宏定义、标签。你可以把公共部分抽成dtsi文件,不同板子引用即可。ATAG?改一个参数得改整个链表。
我曾经踩过的坑: 设备树里reg属性的地址和长度写反了。比如 reg = <0x80000000 0x10000000>,第一个是起始地址,第二个是长度。我写反后内核只认了256MB的前128MB,系统内存直接少一半。排查时用 dtc -I dtb -O dts 反编译dtb,才看到问题。
4.6 实战:Bootloader如何传递DTB
以U-Boot为例,传递DTB的典型流程:
# 加载内核和设备树到内存
tftp 0x82000000 zImage
tftp 0x88000000 myboard.dtb
# 设置启动参数
setenv bootargs console=ttyS0,115200 root=/dev/mmcblk0p2
# 启动内核,传递dtb地址
bootz 0x82000000 - 0x88000000
注意那个 - 符号,它表示没有独立的ramdisk。内核启动时,会从寄存器r2(ARM32)或x2(ARM64)读取DTB地址。
关键检查点:
- DTB地址必须8字节对齐
- DTB不能覆盖内核解压区域
- DTB大小不能超过实际内存
嗯,这里要注意:有些SoC的BootROM会占用低端内存。我遇到过DTB放在0x80000000,结果被BootROM覆盖了,内核启动到一半就挂。后来改到0x88000000才稳定。
4.7 本章小结
ATAG和设备树,本质都是Bootloader和内核之间的「翻译官」。ATAG简单但死板,设备树灵活但复杂。现在新项目基本都用设备树,ARM64更是强制要求。
我个人建议:如果你还在维护老项目,ATAG能跑就别动。但新项目,直接上设备树。别给自己找麻烦。
最后送大家一句话:启动参数传递,看着简单,但出问题最难查。多花10分钟检查dtb地址,能省下10小时调试时间。
避坑指南: 我曾经在设备树里漏了 chosen 节点,结果内核启动参数全是空的。系统起来后串口没输出,我还以为是驱动问题。后来用 printenv 看U-Boot环境变量,才发现bootargs根本没传进去。
可可精品资料库