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 三者的关系画清楚了。我每次带新人时都会先让他们看这张图,理解「源码→二进制→调试」的闭环。

设备树编译与反编译流程 设备树编译与反编译流程 DTS 源码 .dts / .dtsi 文件 DTC 编译 -I dts -O dtb DTB 二进制 .dtb 文件 DTB 二进制 .dtb 文件 DTC 反编译 -I dtb -O dts DTS 反编译结果 可读文本(无注释) 反编译结果不可直接用于重新编译,仅用于调试和查看 编译时加 -@ 选项可保留符号,方便后续 overlay 加载

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,确认改动生效了。这个「编译→反编译→对比」的闭环,能帮你发现很多隐蔽的问题。