第3章 设备树编译器:DTC工具使用、编译与反编译、DTS到DTB的转换、反汇编实战
设备树编译器,简称DTC。说白了,它就是一座桥——把人类能读懂的DTS文本,转成机器能啃的DTB二进制。反过来也能把DTB还原成DTS,这就是反编译。
我刚开始接触设备树时,总觉得这步很简单。不就是敲个命令嘛?结果第一次编译就报错,找了半天才发现是漏了个分号。嗯,细节决定成败,咱们今天就把DTC彻底搞透。
3.1 DTC工具从哪里来?
DTC是Linux内核源码树的一部分。路径在 scripts/dtc/ 下面。你编译内核时,它会被自动编译出来。
我个人习惯直接拿内核源码里的DTC用。当然,你也可以单独安装:
# Ubuntu/Debian
sudo apt-get install device-tree-compiler
# CentOS/RHEL
sudo yum install dtc
# 从内核源码编译
cd linux/scripts/dtc
make
sudo cp dtc /usr/local/bin/
装好后,敲 dtc --version 验证一下。看到版本号就对了。
3.2 编译:从DTS到DTB
编译是最常用的操作。把写好的DTS文件转成DTB,给Bootloader或内核用。
基本命令长这样:
dtc -I dts -O dtb -o my_device.dtb my_device.dts
参数解释:
-I dts:输入格式是DTS文本-O dtb:输出格式是DTB二进制-o:指定输出文件名- 最后一个参数是输入文件
举个例子。假设我们有这么个简单的DTS:
/dts-v1/;
/ {
compatible = "my_company,my_board";
#address-cells = <1>;
#size-cells = <1>;
uart0: serial@10000000 {
compatible = "ns16550";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
};
};
编译它:
dtc -I dts -O dtb -o board.dtb board.dts
没报错的话,就生成了 board.dtb。你可以用 hexdump 或 xxd 看看它的二进制内容:
xxd board.dtb | head -20
你会看到文件头以 0xd00dfeed 开头。这是DTB的魔数,我记了这么多年——"dood feed",挺好玩的。
/dts-v1/; 这个版本声明。没有它,DTC会报错。我刚开始写DTS时经常忘,后来养成了习惯:新建文件第一行就敲这个。
3.3 反编译:从DTB回到DTS
反编译就是把二进制DTB还原成可读的DTS。调试时特别有用。比如你拿到一个别人编译好的DTB,想看看里面到底定义了哪些设备。
命令格式:
dtc -I dtb -O dts -o decompiled.dts board.dtb
参数和编译类似,只是输入输出格式互换了一下。
反编译出来的DTS,和原始DTS不完全一样。它会丢失一些信息:
- 注释全部没了
- 标签(label)可能被展开成路径
- 宏定义(如果有)也被展开了
- 节点顺序可能调整
举个例子。原始DTS里有注释:
/* 这是UART0 */
uart0: serial@10000000 {
...
};
反编译后,注释没了,但节点还在。所以,别指望反编译能100%还原你的原始文件。它只能还原"语义",还原不了"排版"。
实战场景: 我在调试一个ARM开发板时,内核死活不认某个外设。我怀疑是DTB有问题。于是我把内核加载的DTB从 /sys/firmware/devicetree/base 拷出来,反编译一看——果然,某个节点的 reg 属性地址写错了。反编译帮我省了至少半天排查时间。
3.4 反汇编:查看DTB的二进制结构
反汇编和反编译不一样。反汇编是把DTB以二进制结构的形式展示出来,而不是还原成DTS文本。
命令:
dtc -I dtb -O asm -o board.S board.dtb
或者直接用 -O asm 输出到终端:
dtc -I dtb -O asm board.dtb
输出内容长这样:
.dtb {
.magic = 0xd00dfeed;
.totalsize = 0x1a0;
.off_dt_struct = 0x48;
.off_dt_strings = 0x168;
.off_mem_rsvmap = 0x28;
.version = 17;
.last_comp_version = 16;
.boot_cpuid_phys = 0x0;
.size_dt_strings = 0x38;
.size_dt_struct = 0x120;
...
};
这玩意儿平时用得不多。但如果你要深入理解DTB的布局——比如自己写一个DTB解析器,或者调试Bootloader加载DTB的问题——反汇编就是你的好帮手。
DTB的二进制结构分为四个主要区域:
| 区域 | 偏移 | 说明 |
|---|---|---|
| 文件头 | 0x00 | 魔数、总大小、各区域偏移等 |
| 内存保留映射 | off_mem_rsvmap | 保留内存区域列表 |
| 结构块 | off_dt_struct | 节点和属性的树形结构 |
| 字符串块 | off_dt_strings | 所有属性名的字符串池 |
反汇编就是把这四个区域的内容,以汇编伪指令的形式展示出来。你可以清楚地看到每个字节的布局。
3.5 常用选项和调试技巧
DTC还有一些选项,能帮你排查问题:
-V:显示版本号-s:只检查语法,不生成输出文件-W no-unit_address_vs_reg:忽略某些警告-@:生成带__symbols__节点的DTB,支持插件-H:指定phandle格式(legacy或epapr)
我最常用的是 -s 选项。写DTS时,先跑一遍语法检查:
dtc -I dts -s my_device.dts
没报错再正式编译。这能省不少时间。
还有一个 -W 选项,用来控制警告级别。比如你故意用了不推荐写法,但又不想看到警告:
dtc -I dts -O dtb -o output.dtb input.dts -W no-unit_address_vs_reg
sed -i '1s/^\xEF\xBB\xBF//' file.dts 去掉BOM。
3.6 实战:完整的编译-反编译-对比流程
咱们走一遍完整的流程。假设你有一个 test.dts:
/dts-v1/;
/ {
compatible = "acme,coyote";
#address-cells = <1>;
#size-cells = <1>;
gpio0: gpio@2000000 {
compatible = "acme,gpio";
reg = <0x02000000 0x1000>;
gpio-controller;
#gpio-cells = <2>;
};
};
第一步,编译:
dtc -I dts -O dtb -o test.dtb test.dts
第二步,反编译:
dtc -I dtb -O dts -o test_reverse.dts test.dtb
第三步,对比原始文件和反编译文件:
diff test.dts test_reverse.dts
你会发现,反编译出来的文件里,gpio0: 这个标签被展开了,变成了 gpio@2000000。注释也没了。但属性和节点的层级关系完全正确。
这就是DTC的威力。它帮你把人类语言和机器语言之间搭了一座桥。你只要把DTS写对,剩下的交给DTC就行。
总结一下:
- 编译:
dtc -I dts -O dtb -o xxx.dtb xxx.dts - 反编译:
dtc -I dtb -O dts -o xxx.dts xxx.dtb - 反汇编:
dtc -I dtb -O asm xxx.dtb - 语法检查:
dtc -I dts -s xxx.dts
记住这四个命令,设备树编译这块你就拿下了。