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/ 编译
我个人习惯:开发机上用 apt 或 dnf 装,省时间。但如果是做内核源码深度调试,我会用内核自带的 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 指定文件。

注意:如果你的 .dts 文件里用了 #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 会做一些优化,比如去掉注释、重新排列属性顺序。但结构和值不会变。

实战场景:你拿到一块开发板,厂商只给了 .dtb 文件,没有 .dts。怎么办?反编译!我经常用这招来逆向分析别人的设备树配置,学习他们的写法。

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 之间的关系。

设备树编译流程 设备树编译与反编译流程 .dts 源文件 .dtsi 头文件 C 预处理器 (cpp) DTC 编译器 .dtb 二进制 反编译 .dts 编译方向:.dts + .dtsi → cpp 预处理 → DTC → .dtb 反编译方向:.dtb → DTC (-O dts) → .dts

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 解析就乱了。

教训:反编译是验证设备树正确性的最后一道防线。我现在的习惯是,每次修改 .dts 后,都编译再反编译一遍,对照检查 phandle 和地址值对不对。别嫌麻烦,这步能省你半天调试时间。

3.8 小结

DTC 这个工具,说简单也简单,就几个命令。但真正用好它,需要理解编译和反编译背后的逻辑。记住三点:

  • 编译时注意头文件路径,别漏了 -I
  • 反编译是排查问题的利器,特别是 phandle 和地址值
  • 别迷信 .dtb,反编译出来看一眼,心里才有底