Linux内核设备树编译机制深度解析
难度级别:🔴 高级
目标读者:项目维护者、想深入理解设备树编译机制的技术人员
阅读时间:15-20分钟
前置知识:熟悉Makefile、了解设备树基本概念、理解Shell脚本
目录
概述
Linux内核使用两阶段编译流程来处理设备树源文件(.dts):
- 预处理阶段:使用gcc -E处理
#include指令和宏定义 - 编译阶段:使用dtc将预处理后的文件编译成二进制格式(.dtb)
这种设计的优势:
- ✅ 支持C预处理器语法(
#include、#define、#ifdef等) - ✅ 完整的依赖关系管理
- ✅ 支持复杂的条件编译
- ✅ 与内核构建系统无缝集成
核心文件结构
1. 主要Makefile文件
third_party/linux_mainline/
├── scripts/
│ ├── Makefile.dtbs # 设备树编译主Makefile ⭐
│ ├── Makefile.lib # 通用库Makefile
│ └── dtc/
│ ├── include-prefixes/ # 包含前缀符号链接 ⭐
│ └── dtc.c # DTC工具源码
└── arch/arm/boot/dts/
└── Makefile # ARM架构设备树Makefile2. 关键源码位置
文件:third_party/linux_mainline/scripts/Makefile.dtbs
这是设备树编译的核心控制文件,定义了完整的编译规则。
两阶段编译流程
编译命令分析
源码位置:scripts/Makefile.dtbs 第132-137行
quiet_cmd_dtc = DTC $(quiet_dtb_check_tag) $@
cmd_dtc = \
$(HOSTCC) -E $(dtc_cpp_flags) -x assembler-with-cpp -o $(dtc-tmp) $< ; \
$(DTC) -o $@ -b 0 $(addprefix -i,$(dir $<) $(DTC_INCLUDE)) \
$(DTC_FLAGS) -d $(depfile).dtc.tmp $(dtc-tmp) ; \
cat $(depfile).pre.tmp $(depfile).dtc.tmp > $(depfile) \
$(cmd_dtb_check)阶段1:GCC预处理
$(HOSTCC) -E $(dtc_cpp_flags) -x assembler-with-cpp -o $(dtc-tmp) $<参数解析:
$(HOSTCC)- 主机系统的gcc编译器-E- 只进行预处理,不编译$(dtc_cpp_flags)- 预处理标志(后文详解)-x assembler-with-cpp- 指定输入为汇编语言(启用预处理)-o $(dtc-tmp)- 输出到临时文件(.dts.tmp)$<- 第一个依赖文件(.dts文件)
预处理标志(第127行):
dtc_cpp_flags = -Wp,-MMD,$(depfile).pre.tmp -nostdinc -I $(DTC_INCLUDE) -undef -D__DTS__参数说明:
-Wp,-MMD,$(depfile).pre.tmp- 生成预处理依赖文件-nostdinc- 禁用标准C头文件路径(关键!)-I $(DTC_INCLUDE)- 只添加设备树特定的包含路径-undef- 取消所有预定义宏-D__DTS__- 定义设备树编译宏
为什么使用 -x assembler-with-cpp?
这个选项告诉gcc将输入文件视为汇编语言,但启用C预处理器。这样:
- ✅ 支持C预处理语法(
#include、#define) - ✅ 不要求C语法(设备树不是C代码)
- ✅ 允许设备树特有的语法
阶段2:DTC编译
$(DTC) -o $@ -b 0 $(addprefix -i,$(dir $<) $(DTC_INCLUDE)) \
$(DTC_FLAGS) -d $(depfile).dtc.tmp $(dtc-tmp)参数解析:
$(DTC)- 设备树编译器-o $@- 输出文件(.dtb)-b 0- 设备树版本为0(自动检测)-i ...- 添加include搜索路径$(DTC_FLAGS)- DTC编译标志-d $(depfile).dtc.tmp- 生成DTC依赖文件$(dtc-tmp)- 输入文件(预处理后的临时文件)
include路径展开:
$(addprefix -i,$(dir $<) $(DTC_INCLUDE))假设 $< 是 arch/arm/boot/dts/board.dts,展开为:
-i arch/arm/boot/dts/ -i scripts/dtc/include-prefixes阶段3:依赖合并
cat $(depfile).pre.tmp $(depfile).dtc.tmp > $(depfile)将预处理依赖和DTC依赖合并成完整的依赖文件,用于增量编译。
include-prefixes机制
DTC_INCLUDE定义
源码位置:scripts/Makefile.dtbs 第125行
DTC_INCLUDE := $(srctree)/scripts/dtc/include-prefixes目录结构
scripts/dtc/include-prefixes/
├── arc -> ../../../arch/arc/boot/dts
├── arm -> ../../../arch/arm/boot/dts
├── arm64 -> ../../../arch/arm64/boot/dts
├── dt-bindings -> ../../../include/dt-bindings
├── microblaze -> ../../../arch/microblaze/boot/dts
├── mips -> ../../../arch/mips/boot/dts
├── nios2 -> ../../../arch/nios2/boot/dts
├── openrisc -> ../../../arch/openrisc/boot/dts
├── powerpc -> ../../../arch/powerpc/boot/dts
├── riscv -> ../../../arch/riscv/boot/dts
├── sh -> ../../../arch/sh/boot/dts
└── xtensa -> ../../../arch/xtensa/boot/dts工作原理
使用符号链接将架构特定的DTS目录映射到统一的include-prefixes目录:
优势:
- ✅ 架构无关的include路径(
<dt-bindings/...>) - ✅ 自动适配当前编译的架构
- ✅ 简化跨平台设备树的编写
示例:
在设备树中可以这样写:
#include <dt-bindings/interrupt-controller/irq.h>
#include "imx6ull.dtsi" // 自动查找当前架构的目录编译时,dt-bindings会被解析为include/dt-bindings,imx6ull.dtsi会在arch/arm/boot/dts/中查找。
依赖关系管理
双重依赖文件
内核生成两个阶段的依赖文件:
1. 预处理依赖(.pre.tmp)
由gcc -E的-MMD选项生成,记录:
.dts文件包含的所有头文件#include指令引用的文件
2. DTC依赖(.dtc.tmp)
由dtc的-d选项生成,记录:
- DTC工具内部的依赖
- 引用的其他设备树文件
3. 合并依赖
cat $(depfile).pre.tmp $(depfile).dtc.tmp > $(depfile)将两个依赖文件合并,形成完整的依赖关系。
增量编译
依赖文件使得内核构建系统能够:
- ✅ 只重新编译修改过的文件
- ✅ 追踪头文件变化
- ✅ 支持并行编译
完整编译示例
示例设备树文件
文件:arch/arm/boot/dts/board.dts
// SPDX-License-Identifier: (GPL-2.0 OR MIT)
/dts-v1/;
#include "imx6ull.dtsi"
#include "board-common.dtsi"
#include <dt-bindings/interrupt-controller/irq.h>
/ {
model = "Test Board";
compatible = "test,test-board";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>;
};
};实际编译过程
1. 预处理阶段
gcc -E \
-Wp,-MMD,board.dts.pre.tmp \
-nostdinc \
-I scripts/dtc/include-prefixes \
-undef -D__DTS__ \
-x assembler-with-cpp \
-o board.dts.tmp \
arch/arm/boot/dts/board.dts生成的board.dts.tmp(预处理后的内容):
// ... imx6ull.dtsi的内容展开 ...
// ... board-common.dtsi的内容展开 ...
// ... irq.h的内容展开 ...
/dts-v1/;
/ {
model = "Test Board";
compatible = "test,test-board";
memory@80000000 {
device_type = "memory";
reg = <0x80000000 0x10000000>;
};
};2. DTC编译阶段
dtc -o board.dtb \
-b 0 \
-i arch/arm/boot/dts/ \
-i scripts/dtc/include-prefixes \
-Wno-unique_unit_address \
-d board.dtc.tmp \
board.dts.tmp生成文件:
board.dtb- 二进制设备树文件board.dtc.tmp- DTC依赖文件board.dts.tmp- 预处理后的临时文件
3. 依赖合并
cat board.dts.pre.tmp board.dtc.tmp > .board.dtb.d关键参数详解
dtc_cpp_flags完整解析
dtc_cpp_flags = -Wp,-MMD,$(depfile).pre.tmp -nostdinc -I $(DTC_INCLUDE) -undef -D__DTS__| 参数 | 作用 | 原因 |
|---|---|---|
-Wp,-MMD,file | 生成依赖文件 | 追踪头文件变化 |
-nostdinc | 禁用标准头文件 | 避免包含系统头文件 |
-I $(DTC_INCLUDE) | 添加DT特定路径 | 只包含设备树相关文件 |
-undef | 取消预定义宏 | 避免编译器内置宏干扰 |
-D__DTS__ | 定义DTS宏 | 允许条件编译 |
DTC_FLAGS常见设置
DTC_FLAGS += -Wno-unique_unit_address \
-Wno-unit_address_vs_reg \
-Wno-avoid_unnecessary_addr_size \
-Wno-alias_paths \
-Wno-interrupt_map \
-Wno-simple_bus_reg这些标志禁用了一些设备树编译器的警告,因为:
- 设备树可能包含多个相似的节点
- 某些验证规则在不同架构下不适用
符号输出选项(-@)
DTC_FLAGS += $(if $(filter $(patsubst $(obj)/%,%,$@), $(base-dtb-y)), -@)如果设备树是基础DTB(支持overlay),则添加-@选项:
-@- 生成符号信息,允许设备树overlay动态添加节点
编译产物链
完整的编译链路:
源文件:
board.dts
↓ [gcc -E 预处理]
临时文件:
board.dts.tmp (预处理后的DTS)
↓ [dtc 编译]
board.dtb (二进制设备树)
↓ [包装成汇编]
board.dtb.S
↓ [汇编器]
board.dtb.o (目标文件)为什么要包装成目标文件?
- ✅ 将设备树链接到内核镜像
- ✅ 支持模块化设备树
- ✅ 统一的构建流程
总结
Linux内核的设备树编译机制体现了几个重要设计原则:
- 分离关注点:预处理和编译分离
- 依赖管理:完整的依赖追踪
- 跨平台支持:通过符号链接实现架构无关
- 构建系统集成:与Make构建系统无缝集成
关键要点
- ⭐ 使用gcc -E进行预处理,支持完整的C预处理器语法
- ⭐ 使用
-nostdinc隔离设备树编译环境 - ⭐ 通过符号链接实现架构无关的include路径
- ⭐ 双重依赖文件确保增量编译正确性
扩展阅读
- 设备树规范(Devicetree Specification)
- Linux内核设备树文档
- DTC工具源码:
third_party/linux_mainline/scripts/dtc/
下一步:阅读设备树编译迁移指南,了解如何将内核编译机制迁移到独立项目。