3. 设备树编译与反编译

好,咱们进入第三章。这一章我打算聊聊设备树的编译和反编译。说白了,就是怎么把咱们写好的 .dts 文件,变成内核能认的 .dtb 二进制文件,以及反过来怎么从 .dtb 还原出可读的源码。

你可能觉得这步很简单,不就是敲个命令吗?嗯,确实不复杂。但我在项目中遇到过几次因为编译选项不对,导致板子死活起不来的情况。所以,咱们还是得把里面的门道搞清楚。

3.1 DTS 到 DTB:编译过程

设备树编译器(Device Tree Compiler),简称 dtc。它就是咱们手里的“翻译官”。

我个人习惯,在开发阶段会频繁修改设备树,所以编译命令一定要熟记于心。最基本的用法是这样的:

dtc -I dts -O dtb -o my_board.dtb my_board.dts

解释一下参数:

  • -I dts:指定输入格式是 .dts 文本文件。
  • -O dtb:指定输出格式是 .dtb 二进制文件。
  • -o my_board.dtb:指定输出文件名。
  • 最后一个参数就是输入的源文件。

你看,是不是很简单?但这里有个坑。我曾经在调试一块新板子时,编译完烧进去,内核直接 panic。查了半天,发现是 dtc 版本太老,不认识我用的新语法。所以,务必确认你的 dtc 版本。

小提示: 查看 dtc 版本用 dtc --version。Linux 内核源码里也自带了一份 dtc,路径在 scripts/dtc/ 下。用内核自带的通常最保险。

3.2 编译过程中的“预处理”

你可能会问,.dts 文件里那些 #include 和 /include/ 是怎么处理的?

其实,dtc 在正式编译前,会先调用 C 预处理器(cpp)来处理这些包含指令。这也是为什么我们能在 .dts 里写 #include "soc.dtsi" 的原因。

我个人建议,不要手动去展开这些头文件。让工具链自动处理就好。如果你非要看预处理后的结果,可以用 -E 参数:

cpp -nostdinc -I include -undef -x assembler-with-cpp my_board.dts my_board_preprocessed.dts

嗯,这命令有点长。但调试复杂问题时,看看预处理后的文件,能帮你快速定位是哪个宏定义出了问题。

3.3 DTB 到 DTS:反编译过程

反编译,就是把二进制的 .dtb 还原成文本格式的 .dts。这在你拿到一个别人编译好的 .dtb,但没给源码时,特别有用。

命令也很对称:

dtc -I dtb -O dts -o my_board_reversed.dts my_board.dtb

你看,就是把 -I 和 -O 的参数互换一下。

但要注意,反编译出来的 .dts 文件,并不完全等于原始源码。为什么?因为编译过程中,注释会被丢掉,一些宏定义也被展开了。反编译出来的文件,可读性会差一些,所有节点和属性都是展开后的样子。

警告: 不要依赖反编译来恢复你的原始源码!它更适合用来“逆向”理解一个未知的 dtb 文件结构。我曾经见过有人把反编译的文件直接改吧改吧又编译回去,结果系统不稳定。因为丢失了原始的宏定义和注释信息。

3.4 常用工具链

除了 dtc,咱们还有一些好用的工具。我整理了一个表格,方便你查阅:

工具名 用途 我的经验
dtc 编译/反编译设备树 核心工具,必须掌握
fdtdump 以可读格式 dump dtb 内容 快速查看 dtb 结构,比反编译快
fdtget 从 dtb 中读取属性值 脚本里用,不用反编译整个文件
fdtput 修改 dtb 中的属性值 慎用!容易改出问题
fdtlist 列出 dtb 中的节点路径 快速了解 dtb 有哪些设备

这些工具在 dtc 的源码包中通常都包含了。编译 dtc 时,它们会一起被编译出来。

3.5 实战:一个完整的编译流程

咱们来走一遍完整的流程。假设你有一个项目,设备树文件分散在多个目录:

project/
├── dts/
│   ├── my_board.dts
│   └── my_board.dtsi
└── include/
    └── soc.h

编译命令可以这样写:

dtc -I dts -O dtb \
    -i ./dts \
    -i ./include \
    -o output/my_board.dtb \
    dts/my_board.dts

这里用到了 -i 参数,指定搜索路径。这样 dtc 就能找到 #include "soc.h" 和 /include/ "my_board.dtsi" 了。

嗯,这里要注意一点:-i 可以指定多个路径,dtc 会按顺序搜索。

3.6 知识体系结构图

为了让你更直观地理解整个编译与反编译的流程,我画了一张图:

设备树编译与反编译流程 源文件 my_board.dts my_board.dtsi soc.h 编译 dtc -I dts -O dtb 二进制文件 my_board.dtb (机器可读) 反编译 dtc -I dtb -O dts 反编译 .dts (无注释) 常用工具链 dtc (核心) fdtdump fdtget/fdtput fdtlist

这张图清晰地展示了从源文件到二进制文件,再到反编译的完整链路。你可以看到,dtc 是核心枢纽,而其他工具则提供了辅助功能。

3.7 避坑指南

最后,分享几个我踩过的坑:

  • 不要用 -@ 参数乱加插件:这个参数允许在 dtb 中插入插件(overlay)。如果你不需要,别加。加了会让 dtb 变大,而且某些旧版内核不识别。
  • 注意字节序:dtc 默认生成大端序的 dtb。如果你的平台是小端序(比如 ARM),记得加 -E -L 参数?其实不用,dtc 会自动适配。但如果你在 x86 上编译给 ARM 用,要确认一下。
  • 编译警告别忽略:dtc 的警告通常意味着你的设备树有潜在问题。比如 WARNING: unit_address_vs_reg,说明节点名和 reg 属性不匹配。我曾经忽略了这个警告,结果某个外设驱动死活加载不上。

好了,关于编译与反编译,就聊这么多。记住,工具是死的,人是活的。多动手敲命令,遇到问题多看看 dtc --help,你很快就能熟练起来。