3. 设备树编译与反编译:DTC工具使用、DTS到DTB的编译、DTB反编译为DTS、反编译实战技巧

设备树源文件(.dts)是人可读的文本,但U-Boot和内核最终吃的是二进制DTB。这中间的关键桥梁就是DTC——Device Tree Compiler。说白了,它就是设备树的“编译器+反编译器”。

我刚开始接触设备树时,总觉得这步很简单,不就是一条命令嘛。直到有一次在项目中,我反编译出来的DTS跟原始文件对不上,排查了半天才发现是编译时带了特殊参数。嗯,这里面的门道其实不少。

核心认知:DTC不仅是编译工具,更是调试利器。学会反编译,就等于拿到了查看“黑盒”DTB的钥匙。

3.1 DTC工具初探

DTC通常随U-Boot或Linux内核源码一起发布。你可以在 scripts/dtc/ 目录下找到它。我个人习惯在U-Boot根目录下直接编译它:

# 在U-Boot源码根目录
make scripts/dtc/dtc
# 或者直接编译整个工具集
make dtc

编译完成后,scripts/dtc/dtc 就是我们要用的二进制。你也可以把它复制到系统路径下:

cp scripts/dtc/dtc /usr/local/bin/

小提示:不同版本的DTC对设备树语法的支持有细微差异。我建议始终使用与当前U-Boot版本匹配的DTC,避免因版本不一致导致的编译错误。

3.2 DTS到DTB的编译

编译命令的基本格式很简单:

dtc -I dts -O dtb -o output.dtb input.dts

参数说明:

  • -I dts:输入格式为DTS文本
  • -O dtb:输出格式为DTB二进制
  • -o output.dtb:指定输出文件名
  • input.dts:源文件

实际项目中,我们通常还会加几个常用选项:

选项 作用 我什么时候用
-@ 生成插件节点(__symbols__) 需要动态加载overlay时必加
-H epapr 生成EPAPR风格的header PowerPC平台常用
-W no-unit_address_vs_reg 关闭某些警告 处理老旧DTS时经常用
-O dtb -b 0 指定物理CPU启动编号 多核处理器启动时

一个更完整的编译示例:

dtc -I dts -O dtb -@ -W no-unit_address_vs_reg \
    -o my-board.dtb my-board.dts

注意:如果你的DTS文件包含了 #include 指令(通常引用 .dtsi 文件),直接使用 dtc 命令会报错。这时你需要先用C预处理器展开:

cpp -nostdinc -I include -I arch/arm/dts \
    -undef -x assembler-with-cpp \
    my-board.dts my-board-preprocessed.dts
dtc -I dts -O dtb -o my-board.dtb my-board-preprocessed.dts

U-Boot的编译系统(Kbuild)会自动处理这些步骤,但手动调试时你得自己来。

3.3 DTB反编译为DTS

反编译是调试的“后悔药”。拿到一个DTB,你想看看里面到底定义了哪些节点?反编译!

dtc -I dtb -O dts -o output.dts input.dtb

就这么简单。但反编译出来的DTS跟原始DTS长得不太一样,你想想看:

  • 注释全没了——DTB不保存注释
  • 节点顺序可能变化——DTB按phandle排序
  • 属性值格式变了——比如 reg 属性会显示为原始字节数组
  • 标签(label)丢失——标签在编译时被解析掉了

我曾经反编译一个生产环境中的DTB,发现里面多了一个我从来没见过的节点。后来一查,是U-Boot在启动时动态添加的。反编译帮我抓到了这个“幽灵节点”。

3.4 反编译实战技巧

光会跑命令还不够,得会用反编译来解决问题。下面是我总结的几个实战场景:

场景一:验证编译结果

编译完DTB后,反编译回来对比一下:

dtc -I dts -O dtb -o test.dtb test.dts
dtc -I dtb -O dts -o test-decompiled.dts test.dtb
diff test.dts test-decompiled.dts

如果diff有输出,别慌。通常是注释、标签、格式差异。重点关注节点结构和属性值是否一致。

场景二:提取U-Boot使用的DTB

U-Boot启动时,可能从多个位置加载DTB(FIT镜像、分区、网络)。你可以把U-Boot内存中的DTB dump出来反编译:

# 在U-Boot命令行
md.b ${fdtcontroladdr} 0x10000
# 或者用fdt命令
fdt list

然后把内存内容保存成文件,再反编译。我常用这招来确认U-Boot实际用的是哪个DTB。

场景三:对比不同版本的DTB

项目迭代中,DTB经常改。反编译后做diff,能快速定位改动:

dtc -I dtb -O dts -o v1.dts v1.dtb
dtc -I dtb -O dts -o v2.dts v2.dtb
diff -u v1.dts v2.dts

技巧:如果diff输出太多格式差异,可以用 -s 选项让DTC输出更紧凑的格式:

dtc -I dtb -O dts -s -o compact.dts input.dtb

这样输出的DTS没有缩进和换行,对比起来更干净。

3.5 知识体系总览

下面这张图帮你理清DTC工具在整个设备树调试流程中的位置:

DTC工具:编译与反编译知识体系 DTS源文件 (文本,人可读) 预处理 (cpp展开#include) DTC编译 -I dts -O dtb DTB二进制 (二进制,机器可读) DTC反编译 -I dtb -O dts 反编译DTS (无注释/标签) 💡 实战要点 • 编译时加 -@ 选项才能支持overlay • 反编译是调试“黑盒”DTB的唯一手段 • 对比反编译结果时,忽略注释和格式差异,关注节点和属性

3.6 常见问题与避坑

我曾经踩过的坑:

  • 编译时忘记加 -@——导致overlay加载失败,排查了两天
  • 反编译后直接修改再编译——因为标签丢失,编译出来的DTB跟原始行为不一致
  • 使用系统自带的旧版DTC——不支持某些新语法,编译报错

我的建议是:把DTC当成调试工具箱里的“瑞士军刀”。编译时多用 -W 选项开启警告,反编译时多用 -s 选项简化输出。熟练了之后,你会发现设备树调试其实没那么玄乎。