Skip to content

LV100-dtb的文件格式

一、dtb 在内存中是什么样子?

设备树 Blob (DTB) 格式是设备树数据的平面二进制编码。 它用于在软件程序之间交换设备树数据。 例如, 在启动操作系统时, 固件会将 DTB 传递给操作系统内核。

DTB 格式在单个、 线性、 无指针数据结构中对设备树数据进行编码。 它由一个小头部和三个可变大小的部分组成: 内存保留块、 结构块和字符串块。 这些应该以该顺序出现在展平的设备树中。 因此, 设备树结构作为一个整体, 当加载到内存地址时, 将类似于下图

image-20260120172742502

Tips:设备树的 dtb 文件是以大端模式存储,所以打开的时候要注意一下。

二、dtb 格式分析

1. 设备树源码

下面我们以这个设备树为例进行分析:

c
/dts-v1/;
/ {
    model = "This is my devicetree!";
    #address-cells = <1>;
    #size-cells = <1>;
    chosen {
        bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0, 115200";
    };
    cpu1: cpu@1 {
        device_type = "cpu";
        compatible = "arm,cortex-a35", "arm,armv8";
        reg = <0x0 0x1>;
    };
    aliases {
        led1 = "/gpio@22020101";
    };
    node1 {
        #address-cells = <1>;
        #size-cells = <1>;
        gpio@22020102 {
            reg = <0x20220102 0x40>;
        };
    };
    node2 {
        node1-child {
            pinnum = <01234>;
        };
    };
    gpio@22020101 {
        compatible = "led";
        reg = <0x20220101 0x40>;
        status = "okay";
    };
};

而我们之后要分析的是二进制的 dtb 文件, 所以需要使用 dtc 工具将上面的 dts 文件编译成 dtb 文件,前面已经了解过了。

shell
./dtc -I dts -O dtb -o dtb_file_format.dtb dtb_file_format.dts

2. 二进制打开是什么样?

用二进制分析软件(可以用 BinaryViewer 或者 hexdump 命令)打开 dtb 文件并设置大端模式。我这里用的是 BinaryViewer ,这个是免费软件,去官网下载安装就可以了。打开后,内容如下:

image-20250221114541086

Tips: hexdump 命令使用格式如下:

shell
hexdump -C dtb_file_format.dtb

3. 解读二进制文件

3.1 Header

devicetree 的头布局由 struct fdt_header 结构定义。所有的头字段都是 32 位整数, 以大端格式存储。

c
struct fdt_header {
	fdt32_t magic;			 // 设备树头部的魔数
	fdt32_t totalsize;		 // 设备树文件的总大小
	fdt32_t off_dt_struct;	 // 设备树结构体(节点数据) 相对于文件开头的偏移量
	fdt32_t off_dt_strings;  // 设备树字符串表相对于文件开头的偏移量
	fdt32_t off_mem_rsvmap;	 // 内存保留映射表相对于文件开头的偏移量
	fdt32_t version;		 // 设备树版本号
	fdt32_t last_comp_version;	// 最后一个兼容版本号
	/* version 2 fields below */
	fdt32_t boot_cpuid_phys;	// 启动 CPU 的物理 ID
	/* version 3 fields below */
	fdt32_t size_dt_strings;	// 设备树字符串表的大小
	/* version 17 fields below */
	fdt32_t size_dt_struct;	   // 设备树结构体(节点数据) 的大小
};

其中 fdt32_t 就是 uint32_t。每个字段的描述如下所示 :

字段描述
magic该字段为固定值 0xd00dfeed(大端) 。
totalsize该字段包含设备树数据结构的总大小(以字节为单位) 。 此大小应包含结构的所有部分: 标题、 内存保留块、 结构块和字符串块, 以及块之间或最后一个块之后的任何空闲空间间隙。
off_dt_struct该字段包含结构块从头开始的以字节为单位的偏移量。
off_dt_strings该字段包含字符串块从头开始的以字节为单位的偏移量。
off_mem_rsvmap该字段包含从头开始的内存保留块的字节偏移量。
version该字段包含设备树数据结构的版本。
last_comp_version向后兼容的设备树数据结构的最低版本。
boot_cpuid_phys与设备树 CPU 节点的 reg 属性对应
size_dt_strings设备树字符串块部分的字节长度。
size_dt_struct设备树结构块部分的字节长度。

然后来查看二进制文件, 其中 4 个字节表示一个单位, 前十个单位分别代表上述的十个字段如下图:

image-20250221114739533
字段十六进制数值代表含义
magicD00DFEED固定值
totalsize000002A4转换为十进制之后为 676, 表示该文件大小为 676 字节
off_dt_struct00000038表示结构块从 00000038 这个地址开始, 和后面的 size_dt_struct 结构块大小参数一起可以确定结构块的存储范围
off_dt_strings0000024C表示字符串块从 0000024C 这个地址开始, 和后面的 size_dt_strings 字符串块大小参数一起可以确定字符串块的存储范围
off_mem_rsvmap00000028表示内存保留块的偏移为 00000028, header 之后结构快之前都是属于内存保留块。
version0000001111 转换为十进制之后为 17, 表示当前设备树结构版本为 17
last_comp_version0000001010 转换为十进制之后为 16, 表示向前兼容的设备树结构版本为 16
boot_cpuid_phys00000000表示设备树的 teg 属性为 0
size_dt_strings00000058表 示 字 符 串 块 的 大 小 为 00000058 , 和 前 面 的 off_dt_strings 字符串块偏移值一起可以确定字符串块的范围
size_dt_struct00000214表示结构块的大小为 00000214, 和前面的 off_dt_struct 结构块偏移值一起可以确定结构块的范围

3.2 内存保留块

内存保留块(Memory Reserved Block) 是用于客户端程序的保护和保留物理内存区域的列表。 这些保留区域不应被用于一般的内存分配, 而是用于保护重要数据结构, 以防止客户端程序覆盖这些数据。 内存保留块的目的是确保特定的内存区域在客户端程序运行时不被修改或使用。 由于在示例设备树中没有设置内存保留块, 所以相应的区域都为 0, 如下 :

image-20250221115127342

保留区域列表: 内存保留块是一个由一组 64 位大端整数对构成的列表。 每对整数对应一个保留内存区域, 其中包含物理地址和区域的大小(以字节为单位) 。 这些保留区域应该彼此不重叠。

保留区域的用途: 客户端程序不应访问内存保留块中的保留区域, 除非引导程序提供的其他信息明确指示可以访问。 引导程序可以使用特定的方式来指示客户端程序可以访问保留内存的部分内容。 引导程序可能会在文档、 可选的扩展或特定于平台的文档中说明保留内存的特定用途。

格式: 内存保留块中的每个保留区域由一个 64 位大端整数对表示。 每对由 struct fdt_reserve_entry 结构表示

c
struct fdt_reserve_entry {
	fdt64_t address;
	fdt64_t size;
};

其中的第一个整数表示保留区域的物理地址, 第二个整数表示保留区域的大小(以字节为单位) 。 每个整数都以 64 位的形式表示, 即使在 32 位架构上也是如此。 在 32 位 CPU 上,整数的高 32 位将被忽略。

内存保留块为设备树提供了保护和保留物理内存区域的功能。 它确保了特定的内存区域在客户端程序运行时不被修改或使用。 这样可以确保引导程序和其他关键组件在需要的情况下能够访问保留内存的特定部分, 并保护关键数据结构免受意外修改。

3.3 结构块

结构块是设备树中描述设备树本身结构和内容的部分。 它由一系列带有数据的令牌序列组成, 这些令牌按照线性树结构进行组织。

  • (1) 令牌类型

结构块中的令牌分为五种类型, 每种类型用于不同的目的。

a. FDT_BEGIN_NODE (0x00000001): FDT_BEGIN_NODE 标记表示一个节点的开始。 它后面跟着节点的单元名称作为额外数据。 节点名称以以空字符结尾的字符串形式存储, 并且可以包括单元地址。 节点名称后可能需要填充零字节以对齐, 然后是下一个标记, 可以是除了 FDT_END 之外的任何标记。

b. FDT_END_NODE (0x00000002): FDT_END_NODE 标记表示一个节点的结束。 该标记没有额外的数据, 紧随其后的是下一个标记, 可以是除了 FDT_PROP 之外的任何标记。

c. FDT_PROP(0x00000003): FDT_PROP 标记表示设备树中属性的开始。 它后面跟着描述属性的额外数据, 该数据首先由属性的长度和名称组成, 表示为下面这样的结构

c
struct{
	fdt32_t len;
	fdt32_t nameoff;
};

长度表示属性值的字节长度, 名称偏移量指向字符串块中存储属性名称的位置。 在这个结构之后, 属性的值作为字节字符串给出。 属性值后可能需要填充零字节以对齐, 然后是下一个令牌, 可以是除了 FDT_END 之外的任何标记。

d. FDT_NOP (0x00000004): FDT_NOP 令牌可以被解析设备树的程序忽略。 该令牌没有额外的数据, 紧随其后的是下一个令牌, 可以是任何有效的令牌。 使用 FDT_NOP 令牌可以覆盖树中的属性或节点定义, 从而将其从树中删除, 而无需移动设备树 blob 中的其他部分。

e. FDT_END (0x00000009): FDT_END 标记表示结构块的结束。 应该只有一个 FDT_END 标记, 并且应该是结构块中的最后一个标记。 该标记没有额外的数据, 紧随其后的字节应该位于结构块的开头偏移处, 该偏移等于设备树 blob 标头中的 fdt_header.size_dt_struct 字段的值。

  • (2) 树状结构

设备树的结构以线性树的形式表示。 每个节点由 FDT_BEGIN_NODE 标记开始, 由 FDT_END_NODE 标记结束。 节点的属性和子节点在 FDT_END_NODE 之前表示, 因此子节点的 FDT_BEGIN_NODE 和 FDT_END_NODE 令牌嵌套在父节点的令牌中。

  • (3) 结构块的结束

结构块以单个 FDT_END 标记结束。 该标记没有额外的数据, 它位于结构块的末尾, 并且是结构块中的最后一个标记。 FDT_END 标记之后的字节应位于结构块的开头偏移处, 该偏移等于设备树 blob 标头中的 fdt_header.size_dt_struct 字段的值。

对结构块开头的部分内容进行分析:

image-20250221142011534
十六进制数值代表含义
00000001根节点的开始
00000000根节点没有节点名, 所以这里名字为 0
00000003设备树中属性的开始
00000017代表该属性的大小, 换算成十进制为 23, 也就是 "This is my devicetree!" 这一字符串的长度
00000000代表该属性在字符串块的偏移量, 这里为 0, 表示无偏移
54686973 - 65210000model 的具体值

通过使用结构块, 设备树可以以一种层次化的方式组织和描述系统中的设备和资源。 每个节点可以包含属性和子节点, 从而实现更加灵活和可扩展的设备树表示。

3.4 字符串块

字符串块用于存储设备树中使用的 所有属性名称。 它由一系列以空字符结尾的字符串组成, 这些字符串在字符串块中简单地连接在一起, 具体示例如下

image-20250221144542579
  • ( 1) 字符串连接

字符串块中的字符串以空字符(\0) 作为终止符来连接。 这意味着每个字符串都以空字符结尾, 并且下一个字符串紧跟在上一个字符串的末尾。 这种连接方式使得字符串块中的所有字符串形成一个连续的字符序列。

  • ( 2) 偏移量引用

在结构块中, 属性的名称是通过偏移量来引用字符串块中的相应字符串的。 偏移量是一个无符号整数值, 它表示字符串在字符串块中的位置。 通过使用偏移量引用, 设备树可以节省空间, 并且在属性名称发生变化时也更加灵活, 因为只需要更新偏移量, 而不需要修改结构块中的属性引用。

  • ( 3) 对齐约束

字符串块没有对齐约束, 这意味着它可以出现在设备树 blob 的任何偏移处。 这使得字符串块的位置在设备树 blob 中是灵活的, 并且可以根据需要进行调整, 而不会对设备树的解析和处理造成影响。

字符串块是设备树中用于存储属性名称的部分。 它由字符串连接而成, 并通过偏移量在结构块中进行引用。 字符串块的灵活位置使得设备树的表示更加紧凑和可扩展。

三、总结

1. dtb 文件结构图

通过以上分析,可以得到 Device Tree 文件结构如下图(与前面分析的设备树有所不同,这里是从网上找的图)所示。dtb 的头部首先存放的是 fdt_header 的结构体信息,接着是填充区域,填充大小为 off_dt_struct – sizeof(struct fdt_header),填充的值为 0。接着就是 struct fdt_property 结构体的相关信息。最后是 dt_string 部分。

image-20260120173147376

Device Tree 源文件的结构分为 header、fill_area、dt_struct 及 dt_string 四个区域。fill_area 区域填充数值 0。节点(node)信息使用 struct fdt_node_header 结构体描述。属性信息使用 struct fdt_property 结构体描述。各个结构体信息如下:

c
struct fdt_node_header {
	fdt32_t tag;
	char name[0];
};

struct fdt_property {
	fdt32_t tag;
	fdt32_t len;
	fdt32_t nameoff;
	char data[0];
};

struct fdt_node_header 描述节点信息,tag 是标识 node 的起始结束等信息的标志位,name 指向 node 名称的首地址。tag 的取值如下:

c
1. #define FDT_BEGIN_NODE 0x1 /* Start node: full name */
2. #define FDT_END_NODE   0x2 /* End node */
3. #define FDT_PROP 	  0x3 /* Property: name off, size, content */
4. #define FDT_NOP 		  0x4 /* nop */
5. #define FDT_END 		  0x9

FDT_BEGIN_NODEFDT_END_NODE 标识 node 节点的起始和结束,FDT_PROP 标识 node 节点下面的属性起始符,FDT_END 标识 Device Tree 的结束标识符。因此,对于每个 node 节点的 tag 标识符一般为 FDT_BEGIN_NODE,对于每个 node 节点下面的属性的 tag 标识符一般是 FDT_PROP

描述属性采用 struct fdt_property 描述,tag 标识是属性,取值为 FDT_PROP;len 为属性值的长度(包括‘\0’,单位:字节);nameoff 为属性名称存储位置相对于 off_dt_strings 的偏移地址。

2. 设备节点结构图

dt_struct 在 Device Tree 中的结构如下图所示。节点的嵌套也带来 tag 标识符的嵌套。

image-20260120173046807

3. 一个更简单的实例

c
/dts-v1/;
/ {	
	compatible = "hd,test_dts", "hd,test_xxx";
	#address-cells = <0x1>;
	#size-cells = <0x1>;
	model = "HD test dts";
	
	chosen {
		stdout-path = "/ocp/serial@ffff";
	};
	memory@80000000 {
		device_type = "memory";
		reg = <0x80000000 0x10000000>;
	};
	led1:led@2000000 {
		compatible = "test_led";
		#address-cells = <0x1>;
		#size-cells = <0x1>;
		reg = <0x200 0x4>;
	};
};

我们会得到如下结构的 dtb 文件:

image-20250221152833425

整个 dtb 文件还是比较简单的,图中的红色框出的部分为 header 部分的数据,可以看到:

  • (1)fdt_header

第 1 个四字节对应 magic,数据为 D00DFEED.

第 2 四字节对应 totalsize,数据为 000001BC,可以由整张图片看出,这个 dtb 文件的大小由 0x0~0x1bb, 大小刚好是 0x1bc

第 3 个四字节对应 off_dt_struct,数据为 00000038。

第 4 个四字节对应 off_dt_strings,数据为 00000174,可以由整张图片看到,从 0x174 开始刚好是字符串开始的地方

第 5 个四字节对应 off_mem_rsvmap,数据为 00000028

第 6 个四字节对应 version,数据为 00000011,十进制为 17

第 7 个四字节对应 last_comp_version,数据为 00000010,十进制为 16,表示兼容版本 16

第 8 个四字节对应 boot_cpuid_phys,数据为 00000000,仅在版本 2 中使用,这里为 0

第 9 个四字节对应 size_dt_strings,数据为 00000048,表示字符串总长。

第 10 个四字节对应 size_dt_struct,数据为 0000013c,表示 struct 部分总长度。

整个头部为 40 字节,16 进制为 0x28,从头部信息中 off_mem_rsvmap 部分可以得到,reserve memory 起始地址为 0x28,上文中提到,这一部分使用一个 16 字节的 struct 来描述,以一个全为 0 的 struct 结尾。

  • (2)reserve memory

后 16 字节全为 0,可以看出,这里并没有设置 reserve memory。

  • (3)dt_struct

偏移地址来到 0x00000038(0x28+0x10), 接下来 8 个字节为 00000003,根据上述 structure 中的描述,这是 OF_DT_PROP,即标示属性的开始。

接下来 4 字节为 00000018,表明该属性的 value 部分 size 为 24 字节。

接下来 4 字节是当前属性的 key 在 string 部分的偏移地址,这里是 00000000,由头部信息中 off_dt_strings 可以得到,string 部分的开始为 00000174,偏移地址为 0,所以对应字符串为 "compatible".

之后就是 value 部分,这部分的数据是字符串,可以直接从图片右侧栏看出,总共 24 字节的字符串 "hd, test_dts", "hd, test_xxx", 因为字符串之间以 0 结尾,所以程序可以识别出这是两个字符串。

可以看出,到这里,compatible = "hd, test_dts", "hd, test_xxx"; 这个属性就被描述完了。

按照固有的规律,接下来就是对#address-cells = < 0x1 > 的解析,然后是#size-cells = < 0x1 >...

然后就是递归的子节点 chosen,memory@80000000 等等都是按照上文中提到的 structure 解析规则来进行解析,最后以 00000002 结尾。

与根节点不同的是,子节点有一个 unit name,即 chosen,memory@80000000 这些名称,并非节点中的.name 属性。

而整个结构的结束由 00000009 来描述。

  • dt_string

从 0x00000174 地址开始就是 dt_string 的内容了。