4. 设备树编译与反编译:DTC工具使用、dtc命令参数、反编译已有dtb文件。

设备树源文件(.dts)写好了,怎么变成二进制文件(.dtb)给内核用?反过来,拿到一个现成的 .dtb,怎么把它还原成可读的源码?

这就是本章要聊的事。说白了,就是 DTC 这个工具的使用。我最早接触 DTC 时,觉得它不就是个编译器嘛,跟 GCC 差不多。后来发现,它的一些参数和坑,真能让你折腾半天。

核心一句话:DTC(Device Tree Compiler)是设备树世界的“翻译官”,负责在人类可读的 .dts 和机器可读的 .dtb 之间来回转换。

4.1 DTC 工具从哪来?

DTC 的源码在 Linux 内核的 scripts/dtc/ 目录下。你编译内核时,它会被自动编译出来。当然,你也可以单独安装:

# Ubuntu / Debian
sudo apt-get install device-tree-compiler

# Fedora / CentOS
sudo dnf install dtc

# 或者从内核源码编译
make scripts/dtc/dtc

我个人习惯直接用系统包管理器安装,省事。但如果你在搞一个很老的内核,或者需要交叉编译给 ARM 板子用,那就得从内核源码里单独编译了。

4.2 基本用法:编译 .dts → .dtb

这是最常用的操作。命令格式很简单:

dtc -I dts -O dtb -o output.dtb input.dts

参数说明:

参数 含义 说明
-I dts 输入格式 dts 表示源文件,dtb 表示二进制文件,fs 表示文件系统节点
-O dtb 输出格式 同上,dts/dtb/fs 三种
-o 输出文件名 不指定则输出到标准输出
-@ 生成符号节点 用于 overlay 场景,后面章节会讲
-H phandle 格式 legacy/epapr 等,一般不用管

举个例子。我在 Rockchip RK3568 上写过这样一个命令:

dtc -I dts -O dtb -o rk3568-evb.dtb rk3568-evb.dts

编译时如果报错,别慌。大部分错误都是语法问题,比如少了个分号、引号没配对、或者引用了不存在的节点。DTC 的错误信息还算友好,它会告诉你第几行出了什么问题。

小技巧:编译时加上 -Wno-unit_address_vs_reg 可以屏蔽一些关于地址格式的警告。这些警告在严格检查时有用,但日常开发中可以先忽略。

4.3 反编译:从 .dtb 还原 .dts

拿到一块新板子,厂商只给了 .dtb 文件,没有源码?或者你想看看内核实际加载的设备树长什么样?反编译就派上用场了。

dtc -I dtb -O dts -o recovered.dts boot.dtb

就这么简单。但要注意,反编译出来的 .dts 文件,跟原始的 .dts 源文件会有一些差异:

  • 注释全没了:DTC 编译时会丢弃所有注释,反编译自然恢复不了。
  • 宏定义被展开:源文件里用的 #include 和宏,反编译后都变成了具体的值。
  • 节点顺序可能变化:DTC 会按地址排序节点,跟源文件顺序不一定一致。
  • 标签(label)丢失:源文件里的 &uart0 这种引用,反编译后变成了 phandle = <0x...>。

我曾经在调试一个 SPI 驱动时,厂商给的 .dtb 里 SPI 节点工作正常,但源码里就是不行。反编译一看,发现厂商在 .dts 里偷偷加了一个 cs-gpios 属性,而源码里没有。嗯,这种“隐藏改动”在商业板卡里并不少见。

避坑指南:反编译得到的 .dts 只能作为参考,不要直接拿它去修改然后重新编译。因为丢失了宏定义和标签,你改起来会非常痛苦,而且容易出错。正确的做法是找到原始的 .dts 源文件来修改。

4.3 进阶参数:-@ 与 overlay

Rockchip 平台经常用到设备树 overlay(叠加层)。比如你想在不修改主 .dtb 的情况下,动态加载一个外设的配置。这时就需要 -@ 参数。

dtc -@ -I dts -O dtb -o base.dtb base.dts
dtc -@ -I dts -O dtb -o overlay.dtbo overlay.dts

-@ 会在编译时生成符号节点(__symbols__),这样 overlay 才能找到主 .dtb 里的节点并修改它们。没有这个参数,overlay 加载时会报“找不到节点”的错误。

我记得第一次用 overlay 时,忘了加 -@,结果内核报了一堆 __symbols__ not found 的错误。折腾了半小时才发现是编译参数的问题。

4.4 查看 dtb 内容:dtc 的另一种用法

有时候你不想生成文件,只想快速看看 .dtb 里有什么。可以用管道:

dtc -I dtb -O dts boot.dtb | less

或者只查看某个节点:

dtc -I dtb -O dts boot.dtb | grep -A 20 "i2c"

更专业的做法是用 fdtdump 工具(也是 DTC 套件的一部分):

fdtdump boot.dtb | less

fdtdump 的输出格式跟 dtc -O dts 略有不同,它更接近二进制结构的原始表示。我个人更喜欢用 dtc 反编译,可读性更好。

4.5 本章知识结构

下面这张图总结了 DTC 工具的核心工作流:

DTC 工具核心工作流 .dts 源文件 (人类可读) .dtsi 包含文件 (头文件/宏) DTC 编译器 dtc -I dts -O dtb .dtb 二进制 (机器可读) 反编译:dtc -I dtb -O dts 常用参数: -I 输入格式(dts/dtb) | -O 输出格式(dts/dtb) | -o 输出文件 | -@ 生成符号(overlay用) | -W 警告控制

4.6 实战:反编译 Rockchip 的 dtb

假设你有一块 RK3588 的开发板,SD 卡里的 boot.dtb 是厂商编译好的。你想看看它里面关于 i2c2 的配置:

# 反编译整个文件
dtc -I dtb -O dts -o rk3588-evb.dts boot.dtb

# 或者直接过滤
dtc -I dtb -O dts boot.dtb | grep -A 30 "i2c2"

输出大概长这样:

i2c2: i2c@feac0000 {
    compatible = "rockchip,rk3588-i2c", "rockchip,rk3568-i2c";
    reg = <0x0 0xfeac0000 0x0 0x1000>;
    interrupts = <0x0 0x1f9 0x4>;
    clocks = <&cru 0x1b9>, <&cru 0x1ba>;
    clock-names = "i2c", "pclk";
    pinctrl-0 = <&i2c2m0_xfer>;
    pinctrl-names = "default";
    #address-cells = <0x1>;
    #size-cells = <0x0>;
    status = "disabled";
};

注意看,这里的 &cru 和 &i2c2m0_xfer 在反编译后变成了 <0x...> 这样的数字引用。这就是我说的“标签丢失”。如果你想修改这个节点,最好找到原始的 .dts 文件,而不是直接改这个反编译结果。

实用技巧:在 Rockchip 平台上,我经常用反编译来对比两个 .dtb 文件的差异。比如,一个能正常启动,一个不行。反编译后用 diff 命令对比,往往能快速定位问题。

4.7 常见错误与解决

错误信息 原因 解决方法
FATAL ERROR: Unable to parse input tree .dts 文件语法错误 检查分号、引号、括号是否匹配
WARNING: (unit_address_vs_reg) 节点地址与 reg 属性不匹配 加 -Wno-unit_address_vs_reg 忽略
Error: phandle 0x... not found 引用了不存在的节点 检查 &xxx 对应的节点是否存在
__symbols__ not found 编译时没加 -@ 重新用 -@ 参数编译

嗯,这些错误我基本都踩过。尤其是 phandle not found,在大型设备树里特别容易出。我的建议是:写 .dts 时,每引用一个节点,就确认一下它确实存在。别等到编译报错再回头找。