3. 设备树(DTB)二进制格式:FDT结构、魔数、结构块、字符串块

设备树从文本格式(DTS/DSI)经过编译后,生成扁平的二进制文件——DTB (Device Tree Blob)。DTB 遵循 扁平设备树 (Flattened Device Tree, FDT) 规范,这是一种紧凑、可线性扫描的二进制布局。ARM64 启动时,引导加载程序(如 U-Boot)或内核自身需要精确解析此格式,以提取硬件拓扑、内存布局和驱动配置。

3.1 FDT 总体布局

一个合法的 DTB 文件在内存中由四个主要区域顺序组成,每个区域以 4 字节对齐。整体结构如下:

FDT 内存布局(按偏移递增):
struct fdt_header (头部) → memory reservation block (内存预留块) → structure block (结构块) → strings block (字符串块)
DTB 二进制布局总览 fdt_header (40/48 字节) 预留块 (8*N 字节) 结构块 (节点/属性) 字符串块 (属性名) 偏移 0x00 偏移 0x28+ 偏移 0x? 偏移 0x?+

内核或引导程序通过 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 字节 结构块的大小(字节)
调试技巧: 在 ARM64 启动早期,如果内核 panic 并提示 "FDT magic mismatch",通常意味着引导加载程序传递的 DTB 指针错误,或者 DTB 被覆盖。使用 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 (结构块结束)
对齐陷阱: 节点名和属性值数据必须填充到 4 字节边界。例如,节点名 "chosen"(6 字节)后需要填充 2 个字节的 '\0'。解析器在读取节点名时,应读取到 null 终止符,然后跳过填充字节以达到下一个 4 字节对齐地址。
结构块令牌流解析流程 FDT_BEGIN_NODE 节点名 FDT_PROP 属性值 FDT_END_NODE FDT_END 解析器状态机 步骤1: 读取令牌 判断令牌类型 步骤2: 解析数据 节点名/属性值 步骤3: 递归/回溯 构建树形结构 步骤4: 结束 FDT_END 停止 解析器顺序扫描令牌流,遇到 FDT_BEGIN_NODE 则压栈,遇到 FDT_END_NODE 则弹栈 FDT_PROP 后紧跟属性长度、字符串偏移和属性值数据

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"。

调试要点: 在解析 DTB 时,如果属性名显示为乱码或空字符串,请检查 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)编码树形结构,属性值直接内联。
  • 字符串块: 集中存储属性名,结构块中的属性通过偏移引用,节省空间。
  • 内存预留块: 告知内核保留特定物理内存区域,避免被覆盖。