Skip to content

Linux内核设备树编译机制深度解析

难度级别:🔴 高级

目标读者:项目维护者、想深入理解设备树编译机制的技术人员

阅读时间:15-20分钟

前置知识:熟悉Makefile、了解设备树基本概念、理解Shell脚本

目录

概述

Linux内核使用两阶段编译流程来处理设备树源文件(.dts):

  1. 预处理阶段:使用gcc -E处理#include指令和宏定义
  2. 编译阶段:使用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架构设备树Makefile

2. 关键源码位置

文件third_party/linux_mainline/scripts/Makefile.dtbs

这是设备树编译的核心控制文件,定义了完整的编译规则。

两阶段编译流程

编译命令分析

源码位置scripts/Makefile.dtbs 第132-137行

makefile
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预处理

bash
$(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行):

makefile
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编译

bash
$(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路径展开

bash
$(addprefix -i,$(dir $<) $(DTC_INCLUDE))

假设 $<arch/arm/boot/dts/board.dts,展开为:

bash
-i arch/arm/boot/dts/ -i scripts/dtc/include-prefixes

阶段3:依赖合并

bash
cat $(depfile).pre.tmp $(depfile).dtc.tmp > $(depfile)

将预处理依赖和DTC依赖合并成完整的依赖文件,用于增量编译。

include-prefixes机制

DTC_INCLUDE定义

源码位置scripts/Makefile.dtbs 第125行

makefile
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/...>
  • ✅ 自动适配当前编译的架构
  • ✅ 简化跨平台设备树的编写

示例

在设备树中可以这样写:

c
#include <dt-bindings/interrupt-controller/irq.h>
#include "imx6ull.dtsi"  // 自动查找当前架构的目录

编译时,dt-bindings会被解析为include/dt-bindingsimx6ull.dtsi会在arch/arm/boot/dts/中查找。

依赖关系管理

双重依赖文件

内核生成两个阶段的依赖文件:

1. 预处理依赖(.pre.tmp)

由gcc -E的-MMD选项生成,记录:

  • .dts文件包含的所有头文件
  • #include指令引用的文件

2. DTC依赖(.dtc.tmp)

由dtc的-d选项生成,记录:

  • DTC工具内部的依赖
  • 引用的其他设备树文件

3. 合并依赖

bash
cat $(depfile).pre.tmp $(depfile).dtc.tmp > $(depfile)

将两个依赖文件合并,形成完整的依赖关系。

增量编译

依赖文件使得内核构建系统能够:

  • ✅ 只重新编译修改过的文件
  • ✅ 追踪头文件变化
  • ✅ 支持并行编译

完整编译示例

示例设备树文件

文件arch/arm/boot/dts/board.dts

c
// 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. 预处理阶段

bash
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(预处理后的内容):

c
// ... 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编译阶段

bash
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. 依赖合并

bash
cat board.dts.pre.tmp board.dtc.tmp > .board.dtb.d

关键参数详解

dtc_cpp_flags完整解析

makefile
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常见设置

makefile
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

这些标志禁用了一些设备树编译器的警告,因为:

  • 设备树可能包含多个相似的节点
  • 某些验证规则在不同架构下不适用

符号输出选项(-@)

makefile
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内核的设备树编译机制体现了几个重要设计原则:

  1. 分离关注点:预处理和编译分离
  2. 依赖管理:完整的依赖追踪
  3. 跨平台支持:通过符号链接实现架构无关
  4. 构建系统集成:与Make构建系统无缝集成

关键要点

  • ⭐ 使用gcc -E进行预处理,支持完整的C预处理器语法
  • ⭐ 使用-nostdinc隔离设备树编译环境
  • ⭐ 通过符号链接实现架构无关的include路径
  • ⭐ 双重依赖文件确保增量编译正确性

扩展阅读


下一步:阅读设备树编译迁移指南,了解如何将内核编译机制迁移到独立项目。

Built with VitePress