3. 设备树(DTB)二进制格式:FDT结构、魔数、结构块、字符串块
设备树从文本格式(DTS/DSI)经过编译后,生成扁平的二进制文件——DTB (Device Tree Blob)。DTB 遵循 扁平设备树 (Flattened Device Tree, FDT) 规范,这是一种紧凑、可线性扫描的二进制布局。ARM64 启动时,引导加载程序(如 U-Boot)或内核自身需要精确解析此格式,以提取硬件拓扑、内存布局和驱动配置。
3.1 FDT 总体布局
一个合法的 DTB 文件在内存中由四个主要区域顺序组成,每个区域以 4 字节对齐。整体结构如下:
struct fdt_header (头部) → memory reservation block (内存预留块) → structure block (结构块) → strings block (字符串块)
内核或引导程序通过 fdt_header 中的偏移字段定位后三个块。这种设计使得 DTB 可以被原地解析,无需额外的重定位表。
3.2 魔数 (Magic Number) 与头部字段
DTB 头部固定为 40 字节(v16 及以下)或 48 字节(v17+)。解析器首先检查魔数,以确认这是一个有效的 FDT 格式。魔数值为 0xD00DFEED(大端序)。
| 偏移 | 字段名 | 大小 | 描述 |
|---|---|---|---|
| 0x00 | magic |
4 字节 | 魔数,固定 0xD00DFEED(大端) |
| 0x04 | totalsize |
4 字节 | 整个 DTB 文件的总大小(字节) |
| 0x08 | off_dt_struct |
4 字节 | 结构块相对于头部起始的偏移 |
| 0x0C | off_dt_strings |
4 字节 | 字符串块相对于头部起始的偏移 |
| 0x10 | off_mem_rsvmap |
4 字节 | 内存预留块相对于头部起始的偏移 |
| 0x14 | version |
4 字节 | FDT 版本号(当前为 17) |
| 0x18 | last_comp_version |
4 字节 | 向后兼容的最低版本(通常为 16) |
| 0x1C | boot_cpuid_phys |
4 字节 | 引导 CPU 的物理 ID |
| 0x20 | size_dt_strings |
4 字节 | 字符串块的大小(字节) |
| 0x24 | size_dt_struct |
4 字节 | 结构块的大小(字节) |
printk 或 JTAG 读取 fdt_header.magic 地址,确认是否为 0xD00DFEED。
3.3 结构块 (Structure Block)
结构块是 DTB 的核心,以令牌流(token stream)的形式编码了设备树的节点(node)和属性(property)。每个令牌是一个 4 字节的大端整数,后跟令牌特定的数据。解析器通过扫描令牌流来重建树形结构。
3.3.1 令牌类型
| 令牌名称 | 值 | 描述 |
|---|---|---|
FDT_BEGIN_NODE |
0x00000001 | 标记一个节点的开始,后跟节点名(以 null 结尾) |
FDT_END_NODE |
0x00000002 | 标记一个节点的结束 |
FDT_PROP |
0x00000003 | 标记一个属性,后跟属性值长度、属性名在字符串块中的偏移、属性值数据 |
FDT_NOP |
0x00000004 | 空操作,用于填充或占位,解析器应跳过 |
FDT_END |
0x00000009 | 标记结构块的结束 |
3.3.2 结构块解析示例
以下是一个简化的 DTS 片段及其对应的结构块令牌流:
// DTS 片段
/ {
model = "ARM64 Board";
chosen { };
};
编译后的结构块令牌流(大端序,十六进制):
偏移 0x00: 00 00 00 01 // FDT_BEGIN_NODE (根节点)
偏移 0x04: 00 00 00 00 // 节点名 "" (空字符串,根节点)
偏移 0x08: 00 00 00 03 // FDT_PROP (属性)
偏移 0x0C: 00 00 00 0C // 属性值长度 = 12 字节 ("ARM64 Board\0")
偏移 0x10: 00 00 00 00 // 属性名在字符串块中的偏移 (指向 "model")
偏移 0x14: 41 52 4D 36 // "ARM"
偏移 0x18: 34 20 42 6F // "64 B"
偏移 0x1C: 61 72 64 00 // "ard\0"
偏移 0x20: 00 00 00 01 // FDT_BEGIN_NODE (chosen 节点)
偏移 0x24: 63 68 6F 73 // "chos"
偏移 0x28: 65 6E 00 00 // "en\0\0" (4字节对齐填充)
偏移 0x2C: 00 00 00 02 // FDT_END_NODE (chosen 结束)
偏移 0x30: 00 00 00 02 // FDT_END_NODE (根节点结束)
偏移 0x34: 00 00 00 09 // FDT_END (结构块结束)
3.4 字符串块 (Strings Block)
字符串块是一个紧凑的字符串池,存储了所有属性名(property name)。结构块中的 FDT_PROP 令牌并不直接包含属性名字符串,而是包含一个 偏移量,指向字符串块中的某个位置。这种设计避免了属性名在结构块中的重复存储,显著减小了 DTB 体积。
3.4.1 字符串块布局
- 起始位置: 由
fdt_header.off_dt_strings指定。 - 大小: 由
fdt_header.size_dt_strings指定。 - 内容: 连续的、以 null 结尾的字符串,每个字符串对应一个属性名。字符串之间没有分隔符,解析器通过 null 终止符区分。
例如,字符串块可能包含以下内容(十六进制):
偏移 0x00: 6D 6F 64 65 6C 00 // "model\0"
偏移 0x06: 63 6F 6D 70 61 74 69 62 6C 65 00 // "compatible\0"
偏移 0x12: 73 74 61 74 75 73 00 // "status\0"
偏移 0x19: 6D 65 6D 6F 72 79 00 // "memory\0"
如果结构块中某个 FDT_PROP 令牌的字符串偏移字段值为 0x06,则解析器会从字符串块偏移 0x06 处读取字符串,得到 "compatible"。
off_dt_strings + 偏移 是否超出了 size_dt_strings 的范围。常见错误是引导加载程序在复制 DTB 时截断了字符串块。
3.5 内存预留块 (Memory Reservation Block)
内存预留块用于告知内核:某些物理内存区域已被保留(例如用于固件、安全监控或引导加载程序),内核不应将其纳入伙伴系统管理。每个预留条目是一个 16 字节的结构体:
| 偏移 | 字段 | 大小 | 描述 |
|---|---|---|---|
| 0x00 | address |
8 字节 | 预留内存的起始物理地址(大端序) |
| 0x08 | size |
8 字节 | 预留内存的大小(字节,大端序) |
预留块以 address = 0 且 size = 0 的条目结束。ARM64 启动时,内核会解析此块,并在建立页表时将这些区域标记为 reserved。
3.6 编译后的布局验证
使用 dtc 工具可以反编译 DTB 并查看其二进制布局。以下命令将 DTB 转换为可读的 DTS 格式,并输出详细的统计信息:
# 反编译 DTB 为 DTS
dtc -I dtb -O dts -o output.dts boot.dtb
# 查看 DTB 头部信息
dtc -I dtb -O dts -H -o /dev/null boot.dtb 2>&1 | head -20
# 使用 fdtdump 工具直接打印 DTB 结构
fdtdump boot.dtb
输出示例:
// fdtdump 输出片段
/dts-v1/;
// magic: 0xd00dfeed
// totalsize: 0x1234 (4660)
// off_dt_struct: 0x48
// off_dt_strings: 0x1000
// off_mem_rsvmap: 0x28
// version: 17
// last_comp_version: 16
// boot_cpuid_phys: 0x0
// size_dt_strings: 0x234
// size_dt_struct: 0xfb8
/ {
model = "ARM64 Board";
compatible = "arm,arm64-board";
#address-cells = <0x2>;
#size-cells = <0x2>;
...
};
fdtdump 或 dtc -I dtb -O dts 验证 DTB 中的 reg 属性、ranges 属性和 dma-ranges 属性是否与硬件手册一致。错误的地址单元(#address-cells)是导致内存映射失败的常见原因。
3.7 本章小结
- FDT 布局: 头部 → 预留块 → 结构块 → 字符串块,每个块通过头部偏移字段定位。
- 魔数 0xD00DFEED: 解析器首先检查此值,确认 DTB 有效性。
- 结构块: 使用令牌流(BEGIN_NODE、END_NODE、PROP、END)编码树形结构,属性值直接内联。
- 字符串块: 集中存储属性名,结构块中的属性通过偏移引用,节省空间。
- 内存预留块: 告知内核保留特定物理内存区域,避免被覆盖。