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 机制:

设备树编译与覆盖流程 设备树编译与覆盖流程 .dts / .dtsi 源文件 CPP 预处理器 DTC 编译器 .dtb 二进制 DTC 反编译 .dts 可读文本 .dts overlay 源文件 DTC -@ 编译 .dtbo overlay 二进制 内核 configfs / U-Boot 基础 DTB + 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 快速查找节点。比打开文件方便多了。