4、设备树编译器(DTC):编译流程、反编译技巧、常用编译选项

设备树编译器,圈里人都叫它 DTC。说实话,这工具看着不起眼,但你要是用不好它,调试设备树的时候能把你折腾得够呛。我最早接触 DTC 的时候,以为就是个简单的文本转二进制工具,后来踩了不少坑才明白——这玩意儿门道多着呢。

4.1 DTC 的编译流程:从 .dts 到 .dtb

DTC 的工作流程,说白了就三步:

  1. 预处理:处理 #include/include/ 和宏定义
  2. 解析:把 .dts 文本解析成抽象语法树
  3. 输出:根据选项生成 .dtb 或 .dts

我刚开始用的时候,以为 #include 和 C 语言一样是预处理指令。其实 DTC 自己就支持 /include/ 语法,但更常见的做法是用 C 预处理器先跑一遍。嗯,这里要注意:

// 推荐做法:用 cpp 预处理
cpp -nostdinc -I include -undef -x assembler-with-cpp \
    my-board.dts | dtc -O dtb -o my-board.dtb

// 或者直接用 dtc 的 -E 选项(新版本支持)
dtc -E -I dts -O dtb -o my-board.dtb my-board.dts

我个人习惯用第一种方式,因为可以灵活控制 include 路径。你在实际项目中,头文件可能散落在多个目录里,cpp 的 -I 选项比 DTC 自带的路径管理要顺手得多。

4.2 反编译技巧:从 .dtb 回到 .dts

反编译,就是把二进制 .dtb 还原成可读的 .dts。这招在调试时特别有用。我记得有一次,客户给的 .dtb 文件死活点不亮屏幕,我反编译一看——好家伙,时序参数全被编译器优化掉了。

基本命令很简单:

dtc -I dtb -O dts -o decompiled.dts my-board.dtb

但这里有几个坑,我一个个说:

注意:反编译出来的 .dts 会丢失原始注释和宏定义。因为编译时这些信息就没了。你看到的会是展开后的数值,比如 0x12345678 而不是 FOO_BAR

另外,如果你遇到反编译失败的情况,多半是 .dtb 版本不匹配。DTC 的版本和内核版本最好对应上。我曾在一次项目中,用新版本的 DTC 去反编译老内核的 .dtb,结果报了一堆语法错误。

小技巧:反编译时加上 -s 选项,可以保留符号信息。虽然不能恢复宏定义,但至少能保留一些节点顺序的线索。

4.3 常用编译选项详解

DTC 的选项看着多,但常用的就那么几个。我按使用频率排个序:

选项 作用 我的使用场景
-O dtb 输出二进制格式 最终生成给内核用的文件
-O dts 输出文本格式(反编译) 调试、逆向分析
-@ 生成插件(overlay)支持 做动态设备树时必加
-H epapr 指定头部格式 PowerPC 平台常用
-W no-unit_address_vs_reg 关闭某些警告 处理老旧 .dts 时
-V 17 指定 DTSpec 版本 兼容旧内核

这里重点说说 -@ 选项。你想想看,现在很多嵌入式设备都支持 FPGA 动态加载,或者外设热插拔。这时候就需要设备树插件(overlay)。不加 -@ 编译出来的 .dtb,内核是不认 overlay 的。

// 编译支持 overlay 的 .dtb
dtc -@ -I dts -O dtb -o base.dtb base.dts

// 编译 overlay 文件
dtc -@ -I dts -O dtb -o my-overlay.dtbo my-overlay.dts

我曾经在一个项目里,忘了加 -@,结果 overlay 加载时内核报 EPERM。查了两天才发现是编译选项的问题。嗯,从那以后我写 Makefile 都会把 -@ 加上,哪怕当前没用 overlay。

4.4 编译时常见的坑与避坑指南

我这些年用 DTC,踩过的坑能写个小本本。挑几个典型的说说:

坑一:警告当成错误

DTC 默认把某些警告当成错误处理。比如 unit_address_vs_reg 警告,如果你的节点 reg 属性和 unit name 不匹配,编译直接失败。

// 解决方案:用 -W no-xxx 关闭特定警告
dtc -W no-unit_address_vs_reg -I dts -O dtb -o output.dtb input.dts

坑二:大小端问题

DTC 默认用大端输出。但有些 SoC 是小端的。我记得有一次帮客户调试,他们用的小端 ARM 芯片,编译出来的 .dtb 死活不对。后来发现是没指定字节序。

// 强制小端输出
dtc -I dts -O dtb -o output.dtb -BE input.dts  // 大端
dtc -I dts -O dtb -o output.dtb -LE input.dts  // 小端

坑三:内存对齐

.dtb 文件要求 4 字节对齐。如果你手动拼过 .dtb,或者用非标准工具生成过,很容易出现对齐问题。DTC 编译时会自动处理,但反编译再编译就可能出问题。

4.5 进阶技巧:用 DTC 做自动化检查

DTC 不只是个编译器,它还能当静态检查工具用。我经常在 CI 流程里加这么一步:

# 检查设备树语法和语义
dtc -I dts -O dtb -o /dev/null -W all input.dts

# 检查 overlay 兼容性
dtc -@ -I dts -O dtb -o /dev/null -W all overlay.dts

加上 -W all 会开启所有警告。虽然有些警告是误报,但大部分能帮你提前发现问题。比如 graph_child_address 警告,能检查出 display 端口节点地址写没写对。

我的习惯:在 Makefile 里加一个 check-dts 目标,专门跑 DTC 检查。每次提交代码前跑一遍,能省去不少调试时间。

最后说一句,DTC 的版本迭代挺快的。新版本会加一些检查规则,也会修复一些 bug。我建议你定期更新一下 DTC,至少跟上你所用内核的版本。别像我以前那样,用着两年前的 DTC 去编译最新的内核设备树,结果一堆莫名其妙的错误。