3. 设备树编译与反编译:DTC工具使用、dtb与dts互转、反编译实战

设备树这东西,写出来是给人看的(.dts),但机器只认二进制(.dtb)。中间那座桥,就是DTC工具。说白了,DTC就是设备树的编译器,跟gcc之于C语言一个道理。

我刚开始接触设备树时,总觉得这步很简单——不就是编译一下嘛。直到有一次,我在一个ARM64项目上,因为DTC版本不对,编译出来的dtb在uboot里死活加载不了。嗯,从那以后,我对DTC的态度就认真多了。

3.1 DTC工具:设备树的“翻译官”

DTC全称Device Tree Compiler。它干三件事:

  • 把.dts编译成.dtb(文本→二进制)
  • 把.dtb反编译成.dts(二进制→文本)
  • 把.dts和.dtb互相diff对比

大多数Linux发行版都自带DTC。你可以用这个命令检查:

dtc --version
# 输出示例:Version 1.6.1

我个人习惯用1.6.0以上版本。低版本对某些语法支持不好,比如/delete-node/和/delete-property/这些高级操作。

小提示: 如果你在嵌入式开发板上没有DTC,别慌。在PC上编译好dtb,直接拷贝过去用就行。dtb是平台无关的二进制格式。

3.2 dts → dtb:编译实战

最基本的编译命令长这样:

dtc -I dts -O dtb -o my_board.dtb my_board.dts

参数解释:

  • -I dts:输入格式是dts文本
  • -O dtb:输出格式是dtb二进制
  • -o:指定输出文件名

我在项目中经常遇到一个问题:dts里引用了别的.dtsi文件,但编译时找不到路径。这时候要用-i参数指定搜索路径:

dtc -I dts -O dtb -i /path/to/include/dir -o my_board.dtb my_board.dts

还有一个我常用的选项是-@,它会在dtb里生成符号表,方便后续overlay加载:

dtc -@ -I dts -O dtb -o my_board.dtb my_board.dts
注意: 编译时如果报语法错误,DTC会给出行号。但有时候错误信息不太直观。我曾经被一个缺少分号的问题卡了半小时——DTC报的是下一行的错误,而不是真正出错的那一行。所以,检查dts时,多看看报错行的前后几行。

3.3 dtb → dts:反编译实战

反编译就是把二进制还原成文本。这个操作在调试时特别有用。比如你从板子上拿到的dtb,想看看里面到底配了什么:

dtc -I dtb -O dts -o decompiled.dts my_board.dtb

反编译出来的dts,跟原始dts不完全一样。它会丢失注释、宏定义、include关系。但节点结构和属性值是完全保留的。

我记得有一次,客户说他们的网卡在某个内核版本上不工作。我让他们把板子上的dtb dump出来,反编译一看——哦,原来是phy-mode配成了rgmii,但实际硬件是rgmii-id。这种问题,看源码dts不一定能发现,但反编译dtb一看就露馅了。

反编译时,我建议加上-s参数,它会按地址排序节点,方便阅读:

dtc -I dtb -O dts -s -o sorted.dts my_board.dtb

3.4 从板子上获取dtb

实战中,你经常需要从运行中的板子上提取dtb。方法有几种:

方法 命令 说明
sysfs cp /sys/firmware/fdt /tmp/board.dtb 最常用,内核启动时会把dtb暴露在这里
procfs cat /proc/device-tree > /tmp/dt.dump 输出的是文本格式,不是原始dtb
uboot md.b ${fdtaddr} 0x10000 在uboot里用内存dump命令

我个人最常用的是sysfs方法。简单直接:

# 在板子上执行
cp /sys/firmware/fdt /tmp/board.dtb
# 然后传到PC上反编译
dtc -I dtb -O dts -o board.dts board.dtb
核心要点: 从板子上拿到的dtb,是内核真正在用的那个。它可能跟源码里的dts不一样——因为uboot可能会修改dtb(比如修改chosen节点、添加memory节点)。所以调试时,永远以板子上dump出来的dtb为准。

3.5 反编译实战:一个真实案例

去年我做的一个项目,用的是某国产ARM芯片。板子启动时,HDMI输出总是不正常。我怀疑是设备树里显示相关的节点配错了。

步骤是这样的:

  1. 从板子上dump dtb:cp /sys/firmware/fdt /tmp/hdmi.dtb
  2. 反编译:dtc -I dtb -O dts -o hdmi.dts hdmi.dtb
  3. 查看显示控制器节点:
display_controller: display-controller@13800000 {
    compatible = "vendor,display-controller";
    reg = <0x0 0x13800000 0x0 0x10000>;
    interrupts = <0 63 4>;
    clocks = <&clk DISPLAY_CLK>;
    status = "okay";

    port {
        dc_out: endpoint {
            remote-endpoint = <&hdmi_in>;
        };
    };
};

hdmi: hdmi@13900000 {
    compatible = "vendor,hdmi";
    reg = <0x0 0x13900000 0x0 0x10000>;
    interrupts = <0 64 4>;
    clocks = <&clk HDMI_CLK>;
    status = "okay";

    port {
        hdmi_in: endpoint {
            remote-endpoint = <&dc_out>;
        };
    };
};

一看就发现问题了——remote-endpoint的引用关系是对的,但clocks属性里少了一个pixel clock。原始dts里明明有两个clock,但反编译出来的只有1个。这说明uboot或者某个中间环节修改了dtb。

后来查出来,是uboot的fdt处理脚本里,不小心把第二个clock给删了。修复后,HDMI就正常了。

经验之谈: 反编译dtb时,如果发现节点数量或属性跟源码对不上,别急着怀疑DTC。先想想uboot、ATF、或者bootloader有没有对dtb做过手脚。我曾经见过uboot里用fdt_setprop修改了5个属性,结果改错了3个。

3.6 DTC的进阶用法

除了基本的编译反编译,DTC还有一些实用功能:

  • 检查dtb合法性: dtc -I dtb -O dtb my_board.dtb 会重新编译dtb,如果格式有问题会报错
  • 输出依赖关系: dtc -I dts -O dts -M depend my_board.dts 可以查看dts依赖了哪些.dtsi文件
  • 生成C头文件: dtc -I dts -O asm my_board.dts 可以把设备树转成汇编,供bootloader使用

还有一个我经常用的技巧——用DTC做diff。两个dtb文件,想知道哪里不一样?分别反编译,然后用diff对比:

dtc -I dtb -O dts old.dtb > old.dts
dtc -I dtb -O dts new.dtb > new.dts
diff -u old.dts new.dts

这个方法,比直接对比二进制文件直观多了。

3.7 本章知识体系

下面这张图,把DTC工具的使用流程和核心逻辑串起来了:

DTC工具核心流程 .dts 源文件 文本格式,人类可读 DTC 编译器 dtc -I dts -O dtb .dtb 二进制 机器可读,内核使用 反编译:dtc -I dtb -O dts 实战场景:从板子上dump dtb → 反编译 → 分析问题 → 修复dts → 重新编译 常见问题 • 版本不兼容 • include路径错误 • uboot修改dtb 关键参数 -I 输入格式 -O 输出格式 -@ 生成符号表 输出产物 .dtb 内核使用 .dts 反编译调试 .asm bootloader 核心原则:以板子上dump的dtb为准,不要迷信源码

这张图把整个流程串起来了。你从左边开始,写dts,用DTC编译成dtb。内核启动时加载dtb。调试时,从板子上dump dtb,反编译回dts,对比分析。发现问题后,修改dts,重新编译,再部署。循环往复,直到问题解决。

嗯,DTC工具的使用,说白了就是这三板斧——编译、反编译、对比。但真正用好的关键,是理解dtb在系统中的生命周期:从源码到二进制,从bootloader到内核,每一步都可能被修改。你只有掌握了反编译这个逆向手段,才能看清真相。