第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 验证一下。看到版本号就对了。

我的小建议: 尽量用内核自带的DTC版本。不同版本的DTC对语法检查的严格程度不一样。我曾经遇到过用系统自带的旧版DTC编译通过,换到新版就报错的情况。统一版本能省很多麻烦。

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。你可以用 hexdumpxxd 看看它的二进制内容:

xxd board.dtb | head -20

你会看到文件头以 0xd00dfeed 开头。这是DTB的魔数,我记了这么多年——"dood feed",挺好玩的。

注意: DTS文件必须包含 /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 所有属性名的字符串池

反汇编就是把这四个区域的内容,以汇编伪指令的形式展示出来。你可以清楚地看到每个字节的布局。

什么时候用反汇编? 我个人只在两种情况下用:一是怀疑DTB文件被截断或损坏,二是写底层Bootloader时需要手动解析DTB。平时调试,反编译就够用了。

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
避坑指南: 我曾经在一个项目里,DTC编译一直报"FATAL ERROR: Unable to parse input tree"。查了半天,发现是DTS文件编码有问题——文件被保存成了UTF-8 with BOM。DTC不认BOM头。解决办法:用 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

记住这四个命令,设备树编译这块你就拿下了。