3. 设备树编译与反编译
设备树这东西,写出来是给人看的(.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/这些高级操作。
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
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
chosen节点、添加memory节点)。所以调试时,永远以板子上dump出来的dtb为准。
3.5 反编译实战:一个真实案例
去年我做的一个项目,用的是某国产ARM芯片。板子启动时,HDMI输出总是不正常。我怀疑是设备树里显示相关的节点配错了。
步骤是这样的:
- 从板子上dump dtb:
cp /sys/firmware/fdt /tmp/hdmi.dtb - 反编译:
dtc -I dtb -O dts -o hdmi.dts hdmi.dtb - 查看显示控制器节点:
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就正常了。
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工具的使用流程和核心逻辑串起来了:
这张图把整个流程串起来了。你从左边开始,写dts,用DTC编译成dtb。内核启动时加载dtb。调试时,从板子上dump dtb,反编译回dts,对比分析。发现问题后,修改dts,重新编译,再部署。循环往复,直到问题解决。
嗯,DTC工具的使用,说白了就是这三板斧——编译、反编译、对比。但真正用好的关键,是理解dtb在系统中的生命周期:从源码到二进制,从bootloader到内核,每一步都可能被修改。你只有掌握了反编译这个逆向手段,才能看清真相。