3. 设备树编译与反编译:DTC工具使用、DTS到DTB的编译、DTB到DTS的反编译、反编译实战技巧
设备树源文件(.dts)写完后,不能直接给内核用。你得把它编译成二进制格式(.dtb),内核才能解析。反过来,拿到一个现成的 .dtb,想看看里面到底描述了哪些硬件,就得反编译回 .dts。
这套编译/反编译工作,全靠一个工具——DTC(Device Tree Compiler)。
我个人习惯把 DTC 比作「硬件描述语言的编译器」。你写的是源码,它产出的是机器码。只不过这里的「机器」是内核的驱动框架。
3.1 DTC 工具:设备树的「编译器」
DTC 是 Linux 内核源码树里自带的工具。路径在 scripts/dtc/ 下。你编译内核时,它会被自动编译出来。当然,你也可以单独安装:
# Ubuntu / Debian
sudo apt-get install device-tree-compiler
# CentOS / RHEL
sudo yum install dtc
# 从内核源码编译
make scripts/dtc
装好后,敲 dtc --version 确认一下。我遇到过几次版本太老导致编译报错的情况,建议至少用 1.4.7 以上版本。
核心能力:DTC 能完成三件事——编译(DTS → DTB)、反编译(DTB → DTS)、以及设备树叠加(DTS overlay)。本章我们重点讲前两个。
3.2 DTS 到 DTB:编译实战
编译命令很简单。假设你有一个 myboard.dts:
dtc -I dts -O dtb -o myboard.dtb myboard.dts
参数说明:
| 参数 | 含义 |
|---|---|
-I dts |
输入格式是 DTS 源码 |
-O dtb |
输出格式是 DTB 二进制 |
-o |
指定输出文件名 |
实际项目中,我很少直接编译单个 .dts。因为一个完整的设备树往往包含多个 .dtsi 头文件。比如:
// myboard.dts
#include "soc.dtsi"
#include "memory.dtsi"
#include "myboard-pinctrl.dtsi"
/ {
model = "MyBoard v2";
compatible = "myvendor,myboard-v2";
// ...
};
编译时,DTC 会自动展开 #include 和 /include/ 指令。但前提是头文件路径要正确。我建议用 -i 参数指定搜索路径:
dtc -I dts -O dtb -i ./include -o myboard.dtb myboard.dts
小技巧:编译时加上 -@ 选项,可以生成符号节点。这对后续做设备树叠加(overlay)非常有用。不加的话,overlay 可能加载失败。
3.3 DTB 到 DTS:反编译实战
反编译是调试的利器。拿到一个 .dtb,不知道里面有什么?反编译看看:
dtc -I dtb -O dts -o decompiled.dts myboard.dtb
参数和编译正好反过来:
| 参数 | 含义 |
|---|---|
-I dtb |
输入格式是 DTB 二进制 |
-O dts |
输出格式是 DTS 源码 |
反编译出来的 .dts 文件,结构和你写的源码基本一致。但有几个地方要注意:
- 注释全部丢失——DTC 编译时会丢弃注释,反编译不会恢复。
- 标签(label)可能被展开——比如
&uart1会变成完整的路径/soc/serial@...。 - 属性值可能被重新排序——DTC 内部会做优化,顺序不一定和源码相同。
我曾经反编译一个厂商提供的 .dtb,发现里面多了一个我从来没见过的节点。查了半天,原来是 bootloader 在启动时动态添加的。嗯,这种「幽灵节点」在反编译时特别容易暴露出来。
3.4 反编译实战技巧
反编译不只是「把二进制转成文本」那么简单。我总结了几个实战中高频使用的场景:
3.4.1 对比两个 DTB 的差异
有时候,同一块板子,不同版本的内核生成的 .dtb 不一样。怎么快速找出差异?
# 反编译两个 DTB
dtc -I dtb -O dts -o v1.dts v1.dtb
dtc -I dtb -O dts -o v2.dts v2.dtb
# 用 diff 对比
diff -u v1.dts v2.dts
这样一眼就能看出哪些节点被修改、新增或删除了。我在调试 PCIe 枚举问题时,就用这招发现厂商偷偷改了一个 bus-range 属性。
3.4.2 提取 DTB 中的特定节点
如果 .dtb 很大,你只关心某个外设(比如 I2C 控制器),可以反编译后用 grep 定位:
dtc -I dtb -O dts myboard.dtb | grep -A 30 "i2c"
或者用 fdtdump 工具(也是 DTC 套件里的)直接打印结构:
fdtdump myboard.dtb | less
fdtdump 的输出格式和 dtc -O dts 略有不同,但胜在速度快,适合快速浏览。
3.4.3 验证 DTB 是否损坏
有时候从 flash 里读出来的 .dtb 可能被破坏了。怎么验证?
# 尝试反编译,如果 DTB 损坏,dtc 会报错
dtc -I dtb -O dts -o check.dts suspect.dtb
# 或者用 dtc 的 -H 选项做完整性检查
dtc -I dtb -O dtb -o /dev/null suspect.dtb
如果第二条命令没有报错,说明 DTB 结构是完整的。我遇到过几次 flash 读取时序不对,导致 DTB 前几个字节被截断的情况,用这个方法一测就现原形。
注意:反编译出来的 .dts 文件,不能直接拿回去重新编译。因为丢失了注释、标签和原始格式,重新编译可能会产生不同的二进制。反编译只适合「查看」和「调试」,不适合「修改后回写」。
3.5 核心流程:编译与反编译全景
下面这张图,把 DTS、DTB、DTC 三者的关系画清楚了。我每次带新人时都会先让他们看这张图,理解「源码→二进制→调试」的闭环。
3.6 常见问题与避坑
最后,分享几个我踩过的坑:
- 编译报错「syntax error」——八成是
.dtsi文件没找到。检查-i路径,或者用绝对路径。 - 反编译出来的文件特别大——可能是 DTB 里嵌了多个设备树(multi-dtb)。用
dtc -I dtb -O dts -b 0指定 blob 编号。 - 反编译后节点顺序乱——DTC 内部按字母序排序了。这不是 bug,是特性。习惯就好。
- 编译时提示「FDT_ERR_NOSPACE」——DTB 空间不够了。加
-p 1024参数预留额外空间。
我的习惯:每次修改设备树后,我都会先反编译一遍生成的 .dtb,确认改动生效了。这个「编译→反编译→对比」的闭环,能帮你发现很多隐蔽的问题。