3. 设备树编译器(DTC):从安装到反编译实战
设备树编译器,圈里人都叫它 DTC。说白了,它就是一座桥——把我们手写的 .dts 文本文件,转成内核能啃的 .dtb 二进制。反过来,也能把 .dtb 还原成可读的 .dts,这就是反编译。
我刚开始接触设备树那会儿,总觉得这步可有可无。直到有一次,我在一块新板子上调外设,死活点不亮。折腾了两天,最后反编译了一下 .dtb,才发现是地址写错了。嗯,从那以后,我再也不敢小看 DTC 了。
3.1 DTC 的安装
不同平台安装方式不太一样。我列个表,你一看就明白。
| 平台 | 安装命令 | 备注 |
|---|---|---|
| Ubuntu/Debian | sudo apt-get install device-tree-compiler |
最省事,直接装 |
| Fedora/CentOS | sudo dnf install dtc |
包名就叫 dtc |
| Arch Linux | sudo pacman -S dtc |
一样,包名 dtc |
| 源码编译 | make; make install |
从内核源码 scripts/dtc/ 编译 |
装完之后,验证一下:
$ dtc --version
Version 1.6.1
看到版本号,说明装好了。
3.2 编译设备树:从 .dts 到 .dtb
编译命令很简单,但参数你得搞清楚。
dtc -I dts -O dtb -o myboard.dtb myboard.dts
参数含义:
-I dts:输入格式是 .dts 文本-O dtb:输出格式是 .dtb 二进制-o myboard.dtb:指定输出文件名- 最后一个参数:输入文件
我遇到过不少新手,上来就写 dtc myboard.dts,结果报一堆错。为什么?因为 DTC 默认输出到标准输出,你得用 -o 指定文件。
#include 包含头文件,编译时必须加 -I 参数指定头文件路径。比如:
dtc -I dts -O dtb -o myboard.dtb -I /path/to/include myboard.dts
我曾经因为忘了加这个路径,编译出来的 .dtb 少了好几个节点,板子直接起不来。
3.3 反编译设备树:从 .dtb 回到 .dts
反编译,就是把二进制还原成文本。命令格式几乎一样,只是输入输出反过来:
dtc -I dtb -O dts -o myboard.dts myboard.dtb
参数变化:
-I dtb:输入是 .dtb 二进制-O dts:输出是 .dts 文本
反编译出来的 .dts 文件,和原始 .dts 不完全一样。DTC 会做一些优化,比如去掉注释、重新排列属性顺序。但结构和值不会变。
3.4 反编译实战分析
咱们拿一个实际例子走一遍。假设你有一个 vexpress-v2p-ca9.dtb 文件,这是 ARM 官方模拟器用的设备树。
第一步,反编译:
dtc -I dtb -O dts -o vexpress.dts vexpress-v2p-ca9.dtb
第二步,打开 vexpress.dts,你会看到类似这样的结构:
/dts-v1/;
/ {
model = "V2P-CA9";
compatible = "arm,vexpress,v2p-ca9", "arm,vexpress";
interrupt-parent = <0x1>;
#address-cells = <0x1>;
#size-cells = <0x1>;
memory@60000000 {
device_type = "memory";
reg = <0x60000000 0x40000000>;
};
chosen {
bootargs = "console=ttyAMA0,115200 root=/dev/mmcblk0p2 rw";
stdout-path = "serial0:115200n8";
};
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0x0>;
clock-frequency = <0x2faf080>;
};
// ... 更多节点
};
注意几个关键点:
/dts-v1/;:版本声明,反编译后自动加上memory@60000000:内存节点,地址 0x60000000,大小 0x40000000(1GB)chosen:内核启动参数,这里指定了串口和根文件系统cpu@0:CPU 节点,频率 0x2faf080(50MHz)
0x40000000 当成 40000000 字节,结果内存配置错了。记住,0x40000000 是 1GB,不是 40MB。
3.5 设备树编译流程全景图
下面这张图,把整个流程串起来了。你一看就明白 .dts、.dtsi、.dtb 之间的关系。
3.6 常用 DTC 选项速查
我平时用得最多的几个选项,整理成了一张表。你收藏一下,用的时候直接查。
| 选项 | 说明 | 示例 |
|---|---|---|
-I |
指定输入格式 | -I dts 或 -I dtb |
-O |
指定输出格式 | -O dtb 或 -O dts |
-o |
指定输出文件名 | -o myboard.dtb |
-i |
添加头文件搜索路径 | -I /path/to/include |
-@ |
生成插件节点(overlay 用) | dtc -@ ... |
-H |
指定 phandle 格式 | -H epapr |
-V |
指定设备树版本 | -V 17 |
-@ 选项编译出来的 .dtb,在旧版 U-Boot 上加载会报错。后来查了文档才知道,-@ 会生成额外的符号节点,旧版解析器不认识。所以,如果你的 U-Boot 版本比较老,别加 -@。
3.7 反编译实战:排查问题
反编译最大的用处,就是排查问题。我给你说个真实案例。
有一次,我同事说他的网卡驱动加载失败。内核日志里报 of_get_named_gpio_flags: can't parse gpios property。他怀疑是设备树里 GPIO 配错了。
我让他把 .dtb 反编译出来,一看:
ethernet@18000000 {
compatible = "davicom,dm9000";
reg = <0x18000000 0x2 0x18000010 0x2>;
interrupt-parent = <0x2>;
interrupts = <0x1f 0x4>;
davicom,no-eeprom;
local-mac-address = [00 00 00 00 00 00];
phy-mode = "mii";
reset-gpios = <0x3 0x11 0x1>;
};
问题出在 reset-gpios 上。0x3 是 GPIO 控制器的 phandle,0x11 是 pin 号(17),0x1 是标志位。看起来没问题啊?
再仔细看 interrupt-parent = <0x2>,这个 0x2 指向的是哪个节点?反编译文件里找 phandle = <0x2>,发现指向的是 intc@fffee000,一个中断控制器。但网卡的 reset 引脚应该接到 GPIO 上,而不是中断控制器。
原来,同事在 .dts 里写 reset-gpios = <&gpio0 17 GPIO_ACTIVE_LOW>,但 gpio0 的 phandle 在编译时被分配成了 0x3,而 interrupt-parent 的 0x2 是另一个节点。DTC 编译时没有报错,但运行时 GPIO 解析就乱了。
3.8 小结
DTC 这个工具,说简单也简单,就几个命令。但真正用好它,需要理解编译和反编译背后的逻辑。记住三点:
- 编译时注意头文件路径,别漏了
-I - 反编译是排查问题的利器,特别是 phandle 和地址值
- 别迷信 .dtb,反编译出来看一眼,心里才有底