我想应该很少人知道这个,所以拉出来唠唠!
仓库已经开源!所有教程,主线内核移植,跑新版本imx-linux/uboot都在这里!欢迎各位大佬观摩!喜欢的话点个⭐!
仓库地址:https://github.com/Awesome-Embedded-Learning-Studio/imx-forge
静态网页:https://awesome-embedded-learning-studio.github.io/imx-forge/
这是笔者在维护 imx-forge 项目时记录的一篇技术笔记,希望能帮助大家理解 Linux 内核中设备树的编译机制。
如果你在开发嵌入式 Linux 系统,一定接触过设备树(Device Tree)。那些 .dts 文件最终是如何变成内核可识别的 .dtb 二进制文件的?这个过程中 GCC 和 DTC 又是如何协作的?
今天我们就来深入剖析 Linux 内核中设备树的两阶段编译流程,看看这背后巧妙的设计。
Linux 内核处理设备树源文件(.dts)时,采用了一个精妙的两阶段编译策略:
源文件 (.dts)
↓
【阶段1】GCC 预处理 → 处理 #include 和宏定义
↓
临时文件 (.dts.tmp)
↓
【阶段2】DTC 编译 → 生成二进制格式
↓
目标文件 (.dtb)
直接用 DTC 编译不就行了吗?为什么要先用 GCC 处理一遍?这个设计带来了几个显著优势:
✅ 完整的 C 预处理器支持:可以使用 #include、#define、#ifdef 等熟悉的语法 ✅ 强大的依赖管理:自动追踪头文件变化,实现增量编译 ✅ 灵活的条件编译:根据不同配置生成不同版本的设备树 ✅ 与内核构建系统无缝集成:统一管理编译流程
让我们从内核源码入手,看看这个编译过程是如何实现的。核心逻辑位于 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)
这个命令看似复杂,实际上就是三个步骤的串联。我们逐个拆解。
$(HOSTCC) -E $(dtc_cpp_flags) -x assembler-with-cpp -o $(dtc-tmp) $<
这里的参数很有讲究:
$(HOSTCC)-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,file | ||
-nostdinc | 禁用标准 C 头文件路径 | |
-I $(DTC_INCLUDE) | ||
-undef | ||
-D__DTS__ |
为什么使用 -x assembler-with-cpp?
这是个巧妙的技巧。它告诉 GCC:"把输入文件当汇编语言处理,但启用 C 预处理器"。这样做的好处是:
✅ 支持完整的 C 预处理语法(#include、#define、#ifdef) ✅ 不要求符合 C 语法(设备树毕竟不是 C 代码) ✅ 允许设备树特有的语法结构
$(DTC) -o $@ -b 0 $(addprefix -i,$(dir $<) $(DTC_INCLUDE)) \
$(DTC_FLAGS) -d $(depfile).dtc.tmp $(dtc-tmp)
参数解析:
$(DTC)-o $@.dtb)-b 0-i ...$(DTC_FLAGS)-d $(depfile).dtc.tmp$(dtc-tmp)include 路径展开:
$(addprefix -i,$(dir $<) $(DTC_INCLUDE))
假设 $< 是 arch/arm/boot/dts/board.dts,这行会展开为:
-i arch/arm/boot/dts/ -i scripts/dtc/include-prefixes
cat $(depfile).pre.tmp $(depfile).dtc.tmp > $(depfile)
将预处理依赖和 DTC 依赖合并,形成完整的依赖关系链。
这是内核设备树编译系统中最优雅的设计之一。
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/...> 这样的统一写法 ✅ 自动适配当前编译的架构:编译 ARM 时自动指向 ARM 目录 ✅ 简化跨平台设备树的编写:同一份设备树可以在不同架构间复用
实际示例:
在设备树中可以这样写:
#include <dt-bindings/interrupt-controller/irq.h>
#include "imx6ull.dtsi" // 自动查找当前架构的目录
编译时:
dt-bindingsinclude/dt-bindingsimx6ull.dtsiarch/arm/boot/dts/ 中查找内核构建系统非常重视依赖追踪,对于设备树编译,它生成了两个阶段的依赖文件:
由 gcc -E 的 -MMD 选项生成,记录:
.dts#include由 dtc 的 -d 选项生成,记录:
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>;
};
};
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>;
};
};
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.dtbboard.dtc.tmpboard.dts.tmpcat board.dts.pre.tmp board.dtc.tmp > .board.dtb.d
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),则添加 -@ 选项:
完整的编译链路:
源文件:
board.dts
↓ [gcc -E 预处理]
临时文件:
board.dts.tmp (预处理后的 DTS)
↓ [dtc 编译]
board.dtb (二进制设备树)
↓ [包装成汇编]
board.dtb.S
↓ [汇编器]
board.dtb.o (目标文件)
↓ [链接器]
内核镜像或模块
Linux 内核的设备树编译机制体现了几个重要的设计原则:
预处理和编译分离,每个工具做它最擅长的事:
完整的依赖追踪确保:
通过符号链接实现架构无关:
与 Make 构建系统无缝集成:
⭐ 使用 gcc -E 进行预处理:支持完整的 C 预处理器语法,增强表达能力 ⭐ 使用 -nostdinc 隔离环境:避免系统头文件污染,确保可重复构建 ⭐ 通过符号链接实现架构无关:优雅的跨平台解决方案 ⭐ 双重依赖文件确保正确性:预处理和编译两阶段都生成依赖信息
如果你想深入了解设备树的更多细节,可以参考以下资源: