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 文件不是随便堆的二进制数据。它有严格的布局,分为四个主要部分:
- 头部(Header):固定 40 字节,包含魔数、大小、版本号、各部分的偏移量。
- 内存预留块(Memory Reservation Block):告诉内核哪些内存区域不能动。
- 结构块(Structure Block):以 token 形式存储设备树的节点和属性,是核心内容。
- 字符串块(Strings Block):存放属性名,结构块里只存偏移量,节省空间。
我画了一张图,帮你直观理解 dtb 的布局:
结构块里用 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 玩得团团转。