2. DTB文件格式详解:DTB的二进制结构、FDT头部解析、内存保留区、结构块、字符串块
设备树二进制文件(DTB)是设备树源文件(DTS)经过编译器(DTC)编译后生成的二进制格式。理解DTB的二进制结构是排查设备树加载失败、驱动无法匹配等问题的核心基础。DTB遵循扁平设备树(Flattened Device Tree, FDT)规范,其整体布局由四个主要部分组成:FDT头部、内存保留区、结构块和字符串块。
2.1 DTB整体二进制布局
一个标准的DTB文件在内存中的布局顺序是固定的,如下图所示。所有多字节数据(如32位、64位整数)均采用大端序(Big-Endian)存储。
fdtget 或 dtc -I dtb -O dts 解析DTB失败时,首先检查文件头部魔数(Magic Number)是否为 0xD00DFEED。如果魔数错误,说明文件损坏或不是有效的DTB。
2.2 FDT头部解析
FDT头部固定为40字节,位于DTB文件的最开始。头部包含了整个DTB的元数据,用于定位其他三个块。下表列出了头部中所有字段及其含义:
| 偏移(字节) | 字段名 | 大小(字节) | 描述 |
|---|---|---|---|
| 0 | magic | 4 | 魔数,固定为 0xD00DFEED(大端序) |
| 4 | totalsize | 4 | 整个DTB文件的总大小(字节) |
| 8 | off_dt_struct | 4 | 结构块相对于文件起始的偏移地址 |
| 12 | off_dt_strings | 4 | 字符串块相对于文件起始的偏移地址 |
| 16 | off_mem_rsvmap | 4 | 内存保留区相对于文件起始的偏移地址 |
| 20 | version | 4 | DTB版本号,当前主流版本为17 |
| 24 | last_comp_version | 4 | 向后兼容的最低版本号,通常为16 |
| 28 | boot_cpuid_phys | 4 | 引导CPU的物理ID,通常为0 |
| 32 | size_dt_strings | 4 | 字符串块的大小(字节) |
| 36 | size_dt_struct | 4 | 结构块的大小(字节) |
off_dt_struct 或 off_dt_strings 的值超出了 totalsize 的范围,内核在解析时会直接报错并拒绝加载DTB。这通常是由于DTC编译时使用了错误的 -b 参数(指定了错误的物理地址)或文件被截断所致。
2.3 内存保留区
内存保留区用于告知内核哪些物理内存区域不能被操作系统使用(例如被固件、安全监控器或特定硬件保留)。该区域由一组 struct fdt_reserve_entry 结构体组成,每个结构体16字节,以 address=0, size=0 的条目结束。
struct fdt_reserve_entry {
uint64_t address; // 保留内存的起始物理地址(8字节,大端序)
uint64_t size; // 保留内存的大小(8字节,大端序)
};
内存保留区的总大小始终是8的倍数(因为每个条目16字节,且以空条目结束)。
dtc -I dtb -O dts -o /dev/stdout your.dtb | grep -i "memreserve" 可以快速查看DTB中声明的所有保留内存区域。如果驱动申请DMA缓冲区时失败,可以检查是否与保留区冲突。
2.4 结构块
结构块是DTB的核心部分,以令牌(Token)序列的形式存储了设备树的所有节点和属性。每个令牌是一个32位的大端整数,后面可能跟随可变长度的数据。结构块以 FDT_END 令牌结束。
下表列出了结构块中使用的所有令牌及其含义:
| 令牌名称 | 值(32位) | 描述 |
|---|---|---|
| FDT_BEGIN_NODE | 0x00000001 | 标记一个设备树节点的开始,后面紧跟节点名字符串(包含路径名,如 soc 或 uart@1000) |
| FDT_END_NODE | 0x00000002 | 标记一个设备树节点的结束 |
| FDT_PROP | 0x00000003 | 标记一个属性,后面跟随 struct fdt_property 结构体(包含属性值长度、属性名在字符串块中的偏移、属性值数据) |
| FDT_NOP | 0x00000004 | 空操作,用于填充对齐或占位,解析器应跳过 |
| FDT_END | 0x00000009 | 结构块的结束标记 |
结构块中节点和属性的嵌套关系如下所示:
FDT_BEGIN_NODE 时入栈,遇到 FDT_END_NODE 时出栈。属性(FDT_PROP)总是属于当前栈顶的节点。这种嵌套结构保证了设备树的层次关系在二进制层面得以保留。
2.5 字符串块
字符串块是一个简单的字节数组,用于存储所有属性(Property)的名字。结构块中的每个 FDT_PROP 令牌并不直接包含属性名字符串,而是包含一个 偏移量(nameoff),指向字符串块中的某个位置。这种设计避免了属性名在结构块中重复存储,节省了空间。
字符串块的结构特点:
- 字符串块起始于
off_dt_strings指定的偏移地址。 - 块内包含多个以
\0结尾的字符串,例如"compatible\0"、"reg\0"、"status\0"等。 - 字符串块的总大小由头部字段
size_dt_strings指定。 - 属性名在字符串块中的偏移量(nameoff)是相对于字符串块起始地址的偏移。
of_property_read_string 返回 -EINVAL 或 -ENODATA,可能是字符串块中的属性名偏移量(nameoff)超出了 size_dt_strings 的范围。使用 hexdump -C your.dtb 可以手动验证偏移值是否合理。
2.6 字节对齐规则
DTB格式对字节对齐有严格要求,违反对齐规则会导致解析器读取到错误的数据。以下是关键的对齐规则:
- 整体对齐: DTB文件的总大小(totalsize)必须是4的倍数。
- 块对齐: 内存保留区、结构块、字符串块的起始偏移(off_mem_rsvmap、off_dt_struct、off_dt_strings)必须4字节对齐。
- 令牌对齐: 结构块中的每个令牌(FDT_BEGIN_NODE、FDT_END_NODE、FDT_PROP等)必须从4字节对齐的地址开始。
- 属性值对齐: 属性值数据(紧随
struct fdt_property之后)必须4字节对齐,必要时在属性值后面填充FDT_NOP令牌。 - 节点名对齐: 节点名字符串(紧随
FDT_BEGIN_NODE令牌之后)必须4字节对齐,不足时填充\0字节。
fdtput 或十六进制编辑器)后,如果没有重新计算并更新 totalsize 和 size_dt_struct 字段,内核在解析时会因为大小不匹配而报错。建议始终使用 dtc 工具重新编译DTB,避免手动修改二进制文件。
2.7 使用命令行工具验证DTB结构
在实际排查中,以下命令可以帮助你快速验证DTB的二进制结构是否正确:
# 1. 查看DTB头部信息(十六进制)
hexdump -n 40 -v -e '4/1 "%02x " "\n"' your.dtb
# 2. 使用 fdtdump 解析并打印所有节点和属性
fdtdump your.dtb
# 3. 使用 dtc 反编译为 DTS 格式(如果成功,说明结构基本正确)
dtc -I dtb -O dts -o /dev/stdout your.dtb
# 4. 检查结构块大小与头部声明是否一致
# 先获取头部中的 size_dt_struct(偏移36,4字节)
hexdump -s 36 -n 4 -v -e '4/1 "%02x "' your.dtb
# 然后计算实际结构块大小(从 off_dt_struct 到文件末尾减去字符串块大小)