3. 设备树编译与反编译:DTC工具使用、编译为DTB、反编译DTB为DTS、设备树覆盖(overlay)
设备树源文件(.dts)写好了,怎么变成内核能吃的二进制?反过来,拿到一块板子的 .dtb,怎么逆向看它的配置?
这章我们就聊透 DTC 工具,以及它背后的编译、反编译、还有覆盖(overlay)机制。我个人习惯把 DTC 叫做“设备树的编译器”,其实它跟 GCC 的角色很像——输入源码,输出目标文件。
核心一句话:DTC 是设备树生态的基石。不会用 DTC,就等于不会玩设备树。
3.1 DTC 工具:不只是个编译器
DTC(Device Tree Compiler)是内核源码树里自带的工具,路径在 scripts/dtc/ 下。你编译内核的时候,它会被自动编译出来。当然,你也可以单独编译它:
make scripts/dtc
# 或者直接编译安装
make dtc
cp scripts/dtc/dtc /usr/local/bin/
为什么我强调要单独装一份?因为内核里的 DTC 版本可能比较老。我在项目中遇到过,某个新出的 SoC 要求 DTC 版本 ≥ 1.5.0,否则编译出来的 DTB 会有对齐问题。所以我的建议是:从 dtc 官方仓库 拉一份最新版,单独编译安装。
| DTC 版本 | 重要特性 | 我的建议 |
|---|---|---|
| < 1.4.0 | 基本编译/反编译 | 别用了,太老 |
| 1.4.0 - 1.5.0 | 支持 overlay、符号表 | 能用,但小心 bug |
| ≥ 1.5.0 | 稳定 overlay、属性覆盖、插件支持 | 推荐使用 |
3.2 编译为 DTB:从文本到二进制
编译命令很简单:
dtc -I dts -O dtb -o myboard.dtb myboard.dts
参数说明:
-I dts:输入格式是 DTS(文本)-O dtb:输出格式是 DTB(二进制)-o:指定输出文件名
但实际项目中,我很少直接这么用。因为一个完整的设备树通常包含多个 .dtsi 头文件。正确的做法是:
cpp -nostdinc -I include -undef -x assembler-with-cpp myboard.dts myboard_preprocessed.dts
dtc -I dts -O dtb -o myboard.dtb myboard_preprocessed.dts
嗯,这里要注意:DTC 本身不处理 #include 和宏定义。你需要先用 C 预处理器(cpp)展开所有头文件和宏,再把展开后的文件喂给 DTC。内核的 Makefile 里就是这么干的,你去看 arch/arm/boot/dts/Makefile 就明白了。
小技巧:如果你只想快速检查语法,可以用 -@ 选项生成带符号表的 DTB,方便后续 overlay 使用:
dtc -I dts -O dtb -@ -o myboard.dtb myboard.dts
3.3 反编译 DTB 为 DTS:逆向工程利器
拿到一块板子的 .dtb,没有源码怎么办?反编译:
dtc -I dtb -O dts -o myboard_recovered.dts myboard.dtb
反编译出来的 DTS 可读性怎么样?说实话,一般。因为 DTB 里丢失了注释、宏定义、以及原始的文件结构。所有节点会被展开成绝对路径形式:
/ {
soc {
uart@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
};
};
};
我曾经用这个功能救过一次急。客户给了一个闭源的 .dtb,说 SPI 总线频率不对。我反编译一看,发现 spi-max-frequency 被设成了 10MHz,但他们的 Flash 芯片只支持 5MHz。嗯,找到问题就好办了——直接改 DTS 再编译回去。
注意:反编译再编译,不能保证 100% 二进制一致。因为 DTB 里有些元数据(比如节点顺序、属性对齐)在反编译过程中可能丢失。如果你需要精确还原,建议用 dtc -I dtb -O dtb -o copy.dtb original.dtb 做二进制拷贝。
3.4 设备树覆盖(Overlay):动态修改的魔法
Overlay 是什么?说白了,就是设备树的“补丁”。你有一个基础 DTB,然后可以在运行时加载一个 overlay,动态添加或修改节点。这在 FPGA 动态重配置、外设热插拔场景下特别有用。
一个典型的 overlay 文件长这样:
// my_overlay.dts
/dts-v1/;
/plugin/;
/ {
fragment@0 {
target = &uart1;
__overlay__ {
status = "okay";
baud-rate = <115200>;
};
};
fragment@1 {
target-path = "/soc/gpio@10000000";
__overlay__ {
gpio-line-names = "LED_RED", "LED_GREEN", "BTN";
};
};
};
编译 overlay 需要加 -@ 选项:
dtc -I dts -O dtb -@ -o my_overlay.dtbo my_overlay.dts
注意输出后缀是 .dtbo,这是 overlay 二进制文件的约定命名。
加载 overlay 的方式有两种:
- 内核 configfs 接口:
/sys/kernel/config/device-tree/overlays/ - U-Boot 的 fdt overlay 命令:
fdt apply my_overlay.dtbo
我在项目中遇到过一个问题:overlay 加载后,某个 GPIO 中断不工作了。排查了半天,发现是 overlay 里引用的 interrupt-parent 没有显式指定,导致它继承了基础 DTB 里的值,但那个值在 overlay 上下文中是错的。所以我的建议是:overlay 里涉及中断、时钟、DMA 这类跨节点资源时,尽量显式指定 interrupt-parent 和 clocks。
3.5 本章知识体系
下面这张图总结了 DTC 工具链的核心流程和 overlay 机制:
3.6 避坑指南
最后,分享几个我踩过的坑:
- 对齐问题:DTC 默认会对
reg属性做 4 字节对齐。如果你的外设要求 8 字节对齐,记得加dtc -O dtb -R 8。 - 字符串表溢出:当 DTS 里属性名或字符串太长时,DTB 的字符串表可能溢出。我遇到过
compatible字符串超过 64 字节导致内核解析失败的情况。解决办法是缩短字符串,或者升级 DTC 版本。 - Overlay 符号冲突:多个 overlay 同时加载时,如果它们都修改同一个节点,后加载的会覆盖先加载的。这不是 bug,但容易让人困惑。我的习惯是:每个 overlay 只修改自己专属的节点,避免冲突。
调试小技巧:用 dtc -I dtb -O dts -o /dev/stdout myboard.dtb 直接把 DTB 反编译到终端,配合 less 或 grep 快速查找节点。比打开文件方便多了。