4. dtc 编译流程:从 DTS 到 DTB 的完整编译过程
设备树编译器(dtc)是将人类可读的 .dts 源文件转换为二进制 .dtb 设备树 Blob 的核心工具。理解 dtc 的内部编译流程,对于定位语法错误、宏展开异常以及二进制布局问题至关重要。dtc 的编译过程遵循经典编译器的三段式架构:词法分析 → 语法分析 → 代码生成。
核心要点: dtc 并非简单的文本替换工具,它包含完整的词法解析器(Lexer)和语法解析器(Parser),能够检测结构错误、类型不匹配以及节点/属性重复等语义问题。
4.1 编译流程总览
下图展示了从 .dts 源文件到 .dtb 二进制文件的完整编译流水线,以及中间产物与关键处理阶段。
4.2 预处理阶段(C 预处理器)
dtc 在正式编译前,首先调用 C 预处理器(cpp)对 .dts 文件进行预处理。这一阶段处理所有以 # 开头的指令,包括:
- 文件包含: 展开
#include "soc.dtsi"或#include,将外部头文件和.dtsi文件内容递归插入。 - 宏展开: 替换
#define定义的常量,例如#define GPIO_ACTIVE_LOW 1在属性值中被替换为数字1。 - 条件编译: 处理
#ifdef、#if、#endif等指令,根据编译时定义的宏选择性地包含或排除代码块。
调试技巧: 使用
dtc -E 选项可以仅输出预处理后的 .dts 内容,而不进行后续编译。这是排查宏定义错误或包含路径问题的首选方法。
4.3 词法分析(Lexer)
预处理后的文本流进入词法分析器(由 lexer.l 定义)。词法分析器将字符序列分解为有意义的词法单元(Token)。dtc 的词法单元主要包括:
| Token 类型 | 匹配模式 | 示例 |
|---|---|---|
| DT_V1 | /dts-v1/; |
版本标识 |
| LITERAL | 节点名或属性名 | cpu, compatible |
| NUMBER | 十进制/十六进制整数 | 0x100, 42 |
| STRING | 双引号括起的字符串 | "okay" |
| BYTES | 尖括号内的字节序列 | [01 02 03] |
| REF | &label 引用 |
&cpu0 |
| SEMICOLON | ; |
语句结束符 |
| LBRACE/RBRACE | { / } |
节点作用域 |
词法分析器会跳过注释(// 和 /* */)和空白字符。如果遇到无法识别的字符序列,将产生 词法错误,例如未闭合的字符串或非法的特殊字符。
4.4 语法分析(Parser)
Token 流被送入语法分析器(由 yacc/bison 语法文件 dtc-parser.y 定义)。语法分析器根据设备树语法规则,将 Token 序列组织成抽象语法树(AST)。核心语法规则包括:
- 源文件结构:
/dts-v1/;版本声明后跟一个或多个节点定义。 - 节点定义:
[label:] node-name [@unit-address] { properties; child-nodes; }; - 属性赋值:
property-name = value;或property-name;(空属性)。 - 引用与合并:
&label { ... };用于向已定义节点追加内容。
语法分析阶段会检测以下常见错误:
典型语法错误:
- 缺少分号
;结束属性或节点。 - 花括号
{}不匹配。 - 节点名包含非法字符(如空格、
@后跟非十六进制字符)。 - 属性值类型与期望不符(例如字符串赋值给需要
<u32>的属性)。
4.5 语义检查与中间表示
在生成最终二进制之前,dtc 对 AST 进行多轮语义检查和规范化处理:
| 检查项 | 说明 | 错误示例 |
|---|---|---|
| 节点/属性唯一性 | 同一层级下不允许同名节点或属性 | 两个 cpu@0 节点 |
| 标签唯一性 | 标签(label)必须在全局唯一 | cpu0: cpu@0 重复定义 |
| 引用解析 | &label 必须指向已存在的标签 |
&nonexist 未定义标签 |
| 属性值类型 | 检查属性值是否符合绑定(binding)要求 | reg = "string"; 应为 <u32> |
| 地址/大小单元 | 验证 #address-cells 和 #size-cells 的合法性 |
负值或非整数 |
通过语义检查后,dtc 将 AST 转换为内部扁平化表示(Flattened Device Tree),这是一个线性化的结构,便于后续二进制编码。
4.6 代码生成(Code Generation)
最后阶段将内部表示序列化为 .dtb 二进制格式。dtb 文件由以下四部分组成:
- 头部(Header): 包含魔数(
0xd00dfeed)、总大小、结构块和字符串块的偏移量及大小。 - 结构块(Structure Block): 以
FDT_BEGIN_NODE、FDT_PROP等令牌(token)编码的节点和属性树形结构。 - 字符串块(Strings Block): 存储所有属性名和节点名的字符串池,结构块通过偏移量引用。
- 内存预留块(Memory Reservation Block): 记录操作系统不应使用的物理内存区域。
- 字符串去重: 相同的属性名(如
compatible、status)在字符串块中只存储一次。 - 对齐填充: 结构块和字符串块按 4 字节对齐,确保内核访问效率。
- 大小计算: 递归计算所有节点和属性的总大小,填充头部中的偏移量字段。
dtc -I dts -O dtb -o output.dtb input.dts:标准编译。dtc -O dts -o decompiled.dts input.dtb:反编译 dtb 为 dts。dtc -O dtb -@ -o output.dtb input.dts:启用符号支持(overlay 编译)。dtc -V:查看 dtc 版本,不同版本对语法严格度有差异。
代码生成阶段还会执行以下优化:
编译选项速查:
4.7 编译流程总结
下表汇总了 dtc 编译各阶段的输入、输出及关键检查点:
| 阶段 | 输入 | 输出 | 关键检查 |
|---|---|---|---|
| 预处理 | .dts + .dtsi + 头文件 |
展开后的文本流 | 包含路径、宏定义 |
| 词法分析 | 文本流 | Token 序列 | 非法字符、未闭合字符串 |
| 语法分析 | Token 序列 | 抽象语法树(AST) | 分号、花括号、节点名格式 |
| 语义检查 | AST | 扁平化内部表示 | 标签唯一性、引用有效性、类型匹配 |
| 代码生成 | 内部表示 | .dtb 二进制 |
对齐、大小、偏移量 |