3、设备树编译与反编译:dtc工具使用、dtb格式解析、反编译实战

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

答案就是 dtc —— Device Tree Compiler。这玩意儿是设备树生态里的核心工具,说白了就是编译器和反编译器二合一。我刚开始接触设备树时,总觉得这步很简单,不就是敲个命令嘛。直到有一次在项目里遇到 dtb 格式不兼容的问题,折腾了一整天……嗯,从那以后我再也不敢小看 dtc 了。

3.1 dtc 工具:设备树的“翻译官”

dtc 是内核源码树里自带的工具,路径在 scripts/dtc/ 下。你编译内核时,它会被自动编译出来。当然,你也可以单独安装:

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

# CentOS/RHEL
sudo yum install dtc

# 从内核源码编译
make scripts

我个人习惯用内核源码里的 dtc,因为版本和内核匹配,不容易踩坑。你想想看,如果 dtc 版本太老,可能不认识新语法;版本太新,又可能生成旧内核不认识的 dtb 格式。

小技巧: 用 dtc --version 查看版本。内核 5.x 系列对应的 dtc 版本一般在 1.6.x 以上,支持 #include 和 /dts-v1/; 等现代语法。

3.2 编译实战:从 .dts 到 .dtb

基本命令长这样:

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

参数说明:

参数 含义 常用值
-I 输入格式 dts(源文件)、fs(文件系统目录)
-O 输出格式 dtb(二进制)、dts(源文件)、asm(汇编)
-o 输出文件 自定义文件名
-@ 生成插件符号 用于设备树叠加层(overlay)

举个例子,编译我们上一章写的 myboard.dts:

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

如果一切顺利,你会得到一个 myboard.dtb 文件。用 hexdump 或 xxd 看一眼:

xxd myboard.dtb | head -20

你会看到开头的魔数 d0 0d fe ed —— 这是 dtb 格式的标识。嗯,这里要注意,如果魔数不对,内核根本不会认这个文件。

我曾经踩过的坑: 有一次我编译时忘了加 -I dts,dtc 默认输入格式是 dts 所以没报错。但后来我把 .dtb 文件传给 bootloader 时,它直接拒绝加载。排查了半天才发现是 dtc 版本太老,生成的 dtb 格式是 v16,而 bootloader 只认 v17。所以,务必确认 dtc 版本和目标平台匹配。

3.3 dtb 格式解析:二进制里藏着什么?

dtb 文件不是随便堆的二进制数据。它有严格的布局,分为四个主要部分:

  1. 头部(Header):固定 40 字节,包含魔数、大小、版本号、各部分的偏移量。
  2. 内存预留块(Memory Reservation Block):告诉内核哪些内存区域不能动。
  3. 结构块(Structure Block):以 token 形式存储设备树的节点和属性,是核心内容。
  4. 字符串块(Strings Block):存放属性名,结构块里只存偏移量,节省空间。

我画了一张图,帮你直观理解 dtb 的布局:

dtb 文件内部布局 Header(40 字节) 魔数 d0 0d fe ed | 大小 | 版本 | 各块偏移量 Memory Reservation Block 预留内存区域列表(地址 + 大小) Structure Block(核心) Token 序列:BEGIN_NODE | PROP | END_NODE | ... 节点名、属性值以 4 字节对齐存储 Strings Block "compatible" "reg" "interrupts" "status" ... 头部指向各块偏移 结构块引用字符串块

结构块里用 token 来标记节点和属性的开始与结束。常见的 token 有:

Token 名称 值 含义
FDT_BEGIN_NODE 0x00000001 节点开始,后面跟节点名
FDT_END_NODE 0x00000002 节点结束
FDT_PROP 0x00000003 属性开始,后面跟长度、名字偏移、值
FDT_NOP 0x00000004 空操作,用于对齐或占位
FDT_END 0x00000009 结构块结束

你想想看,这种设计是不是很巧妙?属性名不直接存字符串,而是存一个偏移量指向字符串块。这样同一个属性名(比如 compatible)在多个节点里出现时,字符串块里只存一份,省了不少空间。

3.4 反编译实战:从 .dtb 回到 .dts

反编译就是把二进制还原成可读的源码。命令和编译正好反过来:

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

比如反编译我们刚才生成的 myboard.dtb:

dtc -I dtb -O dts -o myboard_recovered.dts myboard.dtb

打开 myboard_recovered.dts,你会发现内容和原始 myboard.dts 几乎一样,但有一些细微差别:

  • 所有十六进制数字都变成了小写(原始文件里可能是大写)。
  • 注释被去掉了(dtb 里不保存注释)。
  • 一些默认值会被显式写出来(比如 #address-cells 和 #size-cells)。
  • 节点顺序可能和原始文件不同(dtc 会按字母序重排)。
实用场景: 拿到一块开发板,厂商只给了 .dtb 文件,没有源码。反编译一下,你就能看到它的设备树结构,了解外设配置。我在做 BSP 移植时经常用这招来“偷师”参考板的配置。

3.5 进阶用法:文件系统目录编译与反编译

dtc 还有一个很酷的功能:直接把一个文件系统目录编译成 dtb。这在做 initramfs 或 overlay 时特别有用。

# 编译目录到 dtb
dtc -I fs -O dtb -o myfs.dtb /path/to/directory

# 反编译 dtb 到目录
dtc -I dtb -O fs -o output_dir myfs.dtb

这个功能会把目录里的每个文件变成设备树里的一个属性,文件名就是属性名,文件内容就是属性值。我个人觉得这招在调试时特别方便,可以快速把一些配置文件塞进设备树里。

3.6 常见问题与避坑指南

最后,分享几个我在项目中遇到的实际问题:

我曾经遇到的坑:
  • 对齐问题:dtb 要求所有数据 4 字节对齐。如果你在 .dts 里写了奇数长度的字符串,dtc 会自动填充 0。但反编译时这些填充 0 会变成 \0 字符,看着很奇怪,其实不影响功能。
  • 版本不匹配:老版本 dtc(比如 1.4.x)生成的 dtb 可能不包含 boot_cpuid_phys 字段,导致多核处理器启动失败。升级 dtc 到 1.6+ 可解决。
  • 符号表丢失:如果你要用设备树叠加层(overlay),编译时必须加 -@ 参数,否则 overlay 加载时会报 symbols not found。

好了,dtc 的基本用法就这些。记住三个核心命令:编译、反编译、检查。多练几次,你就能熟练地把 .dts 和 .dtb 玩得团团转。