深入理解 Zephyr RTOS 编译流程:从 west build 到 ELF 产物的完整拆解
Zephyr 版本: v4.3.0-dev | west 版本: v1.5.0 | 适用架构: ARM Cortex-M(本文以 Cortex-M 为例,其他架构流程类似)
1. 为什么要理解编译流程
Zephyr 的构建系统不是单一工具,而是 west、CMake、Kconfig、DeviceTree、Ninja 五个工具的协作链。每个工具负责不同层面的职责:
| 工具 | 职责 |
|---|---|
| west | 多仓库管理 + 构建命令入口 |
| CMake | 构建系统编排,协调各模块 |
| Kconfig | 软件配置(哪些功能启用、参数值多少) |
| DeviceTree | 硬件描述(有哪些外设、地址在哪、引脚怎么接) |
| Ninja | 构建执行器(并行编译、增量构建) |
理解这条链路,意味着你能回答这些问题:
prj.conf中的配置经过哪些步骤才生效?为什么它的优先级高于板级默认配置?- 写了一个
.overlay文件,构建系统是怎么发现并合并它的? - 自定义的 YAML binding 放在哪个目录才能被识别?
- 链接脚本中的
CONFIG_*和DT_*宏是从哪里来的? - 为什么需要两次链接?第二次链接和第一次有什么不同?
本文从 west build 命令开始,逐层追踪整个编译过程。
本文将追踪以下命令的完整生命周期:
1 | west build -b <board> <app_dir> |
2. 编译全景概览
在深入每个阶段之前,先建立整体认知。一次完整的 west build 分为四个阶段:
flowchart LR
subgraph P1["Phase 1: west 命令"]
A["west build
命令解析"] --> B["build.py
参数组装"]
B --> C["cmake -GNinja
调用 CMake"]
end
subgraph P2["Phase 2: CMake 配置"]
D["CMakeLists.txt
加载链"] --> E["Kconfig
配置合并"]
E --> F["DeviceTree
解析生成"]
F --> G["链接脚本
预处理"]
G --> H["build.ninja
生成"]
end
subgraph P3["Phase 3: Ninja 构建"]
I["源文件编译
.c/.cpp → .obj"] --> J["第 1 次链接
zephyr_pre0.elf"]
J --> K["gen_isr_tables.py
ISR 表生成"]
K --> L["第 2 次链接
zephyr.elf"]
end
subgraph P4["Phase 4: 产物"]
M["zephyr.elf
zephyr.bin
zephyr.hex"]
end
C --> D
H --> I
L --> M
style P1 fill:#e3f2fd,stroke:#1565c0,color:#000
style P2 fill:#e8f5e9,stroke:#2e7d32,color:#000
style P3 fill:#fff3e0,stroke:#ef6c00,color:#000
style P4 fill:#fce4ec,stroke:#c62828,color:#000
每个阶段的输入和输出:
| 阶段 | 输入 | 核心处理 | 输出 |
|---|---|---|---|
| Phase 1 | 命令行参数 | west → build.py → cmake | CMake 进程启动 |
| Phase 2 | 源码 + 配置 + DTS | CMake 配置 | build.ninja + 生成文件 |
| Phase 3 | build.ninja + 源码 |
Ninja → gcc → ld | zephyr.elf |
| Phase 4 | zephyr.elf |
objcopy | .bin / .hex |
3. 环境准备与 west build 启动
3.1 虚拟环境与工具链
Zephyr 使用 Python 虚拟环境管理依赖(west、kconfiglib、dtshlib 等):
1 | # 激活虚拟环境 |
激活后,west、cmake、ninja、python 以及交叉编译工具链(如 arm-zephyr-eabi-gcc)均可用。
3.2 west build 的发现机制:命令如何路由到 build.py
当你敲下 west build 时,west 怎么知道该执行哪个 Python 文件?这涉及一个三步注册链路:
sequenceDiagram
participant User
participant West as west CLI
participant Config as .west/config
participant Manifest as zephyr/west.yml
participant Commands as west-commands.yml
participant Build as build.py
User->>West: west build -b my_board
West->>Config: 读取 manifest 配置
Config-->>West: path = zephyr
West->>Manifest: 查找 west-commands 注册
Manifest-->>West: west-commands: scripts/west-commands.yml
West->>Commands: 查找 "build" 命令
Commands-->>West: file: scripts/west_commands/build.py
class: Build
West->>Build: 实例化 Build 类,调用 do_run()
Build->>Build: _run_cmake() → cmake ...
Build->>Build: _run_build() → cmake --build ...
对应的配置文件链:
1 | # .west/config — Step 1: 指定 manifest 项目路径 |
1 | # zephyr/west.yml — Step 2: 声明扩展命令入口 |
1 | # zephyr/scripts/west-commands.yml — Step 3: 注册具体命令 |
3.3 参数解析与 CMake 调用
build.py 的 do_run() 方法分两步走:
1 | # zephyr/scripts/west_commands/build.py |
Step 1:组装 CMake 参数
1 | # build.py 组装的参数(简化) |
其中 DEFAULT_CMAKE_GENERATOR = 'Ninja' 硬编码在 zcmake.py 中,这是 west 默认使用 Ninja 而非 Make 的原因。
构建目录如何确定
-B 参数指定构建输出目录(即 self.build_dir),这个值由 find_build_dir() 函数按优先级链确定:
1 | # zephyr/scripts/west_commands/build.py |
is_zephyr_build() 通过检查 CMakeCache.txt 等标志文件判断目录是否是有效的 Zephyr 构建目录。build.dir-fmt 支持占位符(如 {board}),通过 west config build.dir-fmt "build/{board}" 配置,可以让不同板子自动使用不同构建目录。
大多数人不配 -d 也不配 dir-fmt,最终走到优先级 5,输出到应用目录下的 build/。
Step 2:CMake 自动调用 Ninja
1 | # zephyr/scripts/west_commands/zcmake.py |
cmake --build 并不直接调用 gcc,而是读取 CMakeCache.txt 中缓存的 CMAKE_GENERATOR=Ninja,然后自动路由到 Ninja 执行器。这就是为什么你在控制台看不到显式的 ninja 命令。
等价于:
1 | # Step 1: CMake 配置(生成构建规则) |
4. CMake 配置阶段
这是整个编译流程中最复杂的阶段。CMake 加载构建脚本、处理配置、解析设备树、生成链接脚本,最终输出 build.ninja。
4.0 CMake 如何找到应用的 CMakeLists.txt
在进入 CMake 配置之前,先回答一个基本问题:CMake 怎么知道要用哪个 CMakeLists.txt?
答案在 west build 传递给 cmake 的 -S 参数中。
west build 的参数解析(build.py):
1 | # west build 的位置参数就是应用源码目录 |
build.py 将其传给 cmake 的 -S 参数:
1 | # build.py 组装 CMake 参数时 |
CMake 收到 -S <app_dir> 后,按标准规则在该目录下查找 CMakeLists.txt:
1 | cmake -S my_app/ -B build/ -G Ninja ... |
这就是为什么每个 Zephyr 应用的根目录下必须有一个 CMakeLists.txt —— 它是 CMake 的标准入口文件,文件名不可更改。
如果没有指定 source_dir:west 默认使用当前工作目录作为 source_dir,所以直接在应用目录下执行 west build -b <board> 也能工作。
4.1 CMakeLists.txt 加载链
一切从应用的 CMakeLists.txt 开始:
1 | # <app_dir>/CMakeLists.txt — 每个应用的构建入口 |
find_package(Zephyr) 触发的加载链:
flowchart TD
A["应用 CMakeLists.txt"] -->|"find_package(Zephyr)"| B["ZephyrConfig.cmake
第 1 跳"]
B -->|"设置 ZEPHYR_BASE
PREPEND CMAKE_MODULE_PATH"| C["zephyr_default.cmake
第 2 跳 — 模块编排器"]
C --> D["python.cmake"]
C --> E["extensions.cmake"]
C --> F["configuration_files.cmake
查找 prj.conf"]
C --> G["boards.cmake
查找 board defconfig"]
C --> H["shields.cmake
查找 shield overlay"]
C --> I["dts.cmake
DeviceTree 处理"]
C --> J["kconfig.cmake
Kconfig 处理"]
C --> K["kernel.cmake"]
K -->|"project(Zephyr-Kernel)"| L["add_subdirectory
zephyr/CMakeLists.txt"]
style A fill:#bbdefb,stroke:#1565c0,color:#000
style C fill:#c8e6c9,stroke:#2e7d32,color:#000
style I fill:#ffe0b2,stroke:#ef6c00,color:#000
style J fill:#ffe0b2,stroke:#ef6c00,color:#000
关键细节:
ZEPHYR_BASE解析优先级:环境变量$ENV{ZEPHYR_BASE}→ CMake Cache → 文件路径推算模块加载顺序由
zephyr_default.cmake的foreach循环控制。DeviceTree 在 Kconfig 之前处理:1
2list(APPEND zephyr_cmake_modules dts) # 先
list(APPEND zephyr_cmake_modules kconfig) # 后安全保护:
zephyr/CMakeLists.txt开头检查ZEPHYR_BINARY_DIR是否已定义,确保自己是被kernel.cmake通过add_subdirectory调用的,而不是被用户误操作直接 cmake
4.2 Kconfig 配置系统详解
Kconfig 管理”软件层面”的配置:哪些功能启用、缓冲区多大、调试输出级别等。它不是简单的键值对拼接,而是一个带依赖关系的配置管理系统。
4.2.1 处理流程
flowchart TD
subgraph S1["阶段 1: 配置文件收集"]
A1["configuration_files.cmake
查找 prj.conf"] --> A2["boards.cmake
查找 board_defconfig"]
A2 --> A3["shields.cmake
查找 shield conf"]
end
subgraph S2["阶段 2: 路径变量设置"]
B1["DOTCONFIG = build/zephyr/.config"]
B2["AUTOCONF_H = build/.../autoconf.h"]
B3["KCONFIG_ROOT = zephyr/Kconfig"]
end
subgraph S3["阶段 3: 增量构建检查"]
C1["MD5(所有配置文件)"] --> C2{"校验和
相同?"}
C2 -->|是| C3["跳过重新生成
复用上次 .config"]
C2 -->|否| C4["继续处理"]
end
subgraph S4["阶段 4: kconfig.py 执行"]
D1["Kconfiglib 解析
Kconfig 树"] --> D2["按优先级
合并配置"]
D2 --> D3["依赖处理
depends on / select"]
D3 --> D4["写出 .config
+ autoconf.h"]
end
subgraph S5["阶段 5: 导入 CMake"]
E1["import_kconfig(.config)"]
E2["CMake 可用 ${CONFIG_xxx}"]
end
S1 --> S2 --> S3 --> S4 --> S5
style S1 fill:#e3f2fd,stroke:#1565c0,color:#000
style S3 fill:#fff3e0,stroke:#ef6c00,color:#000
style S4 fill:#e8f5e9,stroke:#2e7d32,color:#000
style S5 fill:#fce4ec,stroke:#c62828,color:#000
4.2.2 配置文件优先级
Kconfig 按优先级合并配置,后者覆盖前者:
| 优先级 | 文件 | 来源 | 说明 |
|---|---|---|---|
| 1(最低) | <board>_defconfig |
板级默认 | 板子能跑的最基础配置 |
| 2 | <board>_revision.conf |
板级修订 | 特定硬件版本的配置 |
| 3 | 板级扩展 conf | 板级扩展目录 | 额外板级配置 |
| 4 | prj.conf |
应用配置 | 开发者自己写的配置 |
| 5 | Shield conf | Shield 目录 | 扩展板配置 |
| 6 | 额外 conf | 命令行指定 | --extra-conf-file |
| 7(最高) | EXTRA_KCONFIG_OPTIONS |
命令行选项 | 强制覆盖 |
这意味着你的 prj.conf 可以覆盖板级默认,但可以被命令行选项覆盖。
4.2.3 Kconfig 树的解析
kconfig.py 使用 Kconfiglib 从 KCONFIG_ROOT(通常是 zephyr/Kconfig)开始递归解析整棵 Kconfig 树:
1 | # zephyr/scripts/kconfig/kconfig.py 核心逻辑 |
4.2.4 输出文件
**.config**(供 CMake 和脚本读取):
1 | CONFIG_MAIN_STACK_SIZE=2048 |
**autoconf.h**(供 C 源码和链接脚本使用):
1 |
通过 -imacros autoconf.h 编译选项,所有 C 源文件和链接脚本模板都能使用 CONFIG_* 宏。在 CMake 中则通过 import_kconfig() 使 ${CONFIG_xxx} 变量可用。
4.3 DeviceTree 处理详解
如果说 Kconfig 管”软件配置”,那 DeviceTree 管”硬件描述”:SoC 有哪些外设、寄存器地址在哪、中断号是多少、引脚怎么复用。这是编译流程中涉及文件最多、环节最复杂的部分。
4.3.1 处理流程总览
flowchart TD
subgraph S1["阶段 1: 路径与文件发现"]
A1["pre_dt.cmake
构建 DTS_ROOT 搜索路径"] --> A2["dts.cmake
收集 dts_files"]
A2 --> A3["configuration_files.cmake
查找 app overlay"]
A3 --> A4["shields.cmake
查找 shield overlay"]
end
subgraph S2["阶段 2: C 预处理器合并"]
B1["arm-gcc -E
展开 #include 和宏"] --> B2["zephyr.dts.pre
纯 DTS 文本"]
end
subgraph S3["阶段 3: EDT 解析"]
C1["gen_edt.py + edtlib
解析 DTS 语法"] --> C2["匹配 YAML bindings"]
C2 --> C3["验证属性类型"]
C3 --> C4["输出 zephyr.dts
+ edt.pickle"]
end
subgraph S4["阶段 4: C 宏生成"]
D1["gen_defines.py
读取 edt.pickle"] --> D2["生成 DT_* 宏"]
D2 --> D3["devicetree_generated.h"]
end
subgraph S5["阶段 5: dtc 检查"]
E1["dtc 编译器
仅语法检查"] --> E2["输出丢弃"]
end
S1 --> S2 --> S3 --> S4 --> S5
style S1 fill:#e3f2fd,stroke:#1565c0,color:#000
style S2 fill:#e8f5e9,stroke:#2e7d32,color:#000
style S3 fill:#fff3e0,stroke:#ef6c00,color:#000
style S4 fill:#fce4ec,stroke:#c62828,color:#000
4.3.2 阶段 1: DTS 文件发现与优先级
DTS_ROOT 搜索路径构建(pre_dt.cmake):
1 | list(APPEND DTS_ROOT |
对每个 DTS_ROOT 目录,搜索以下子路径作为 #include 搜索目录:
include/— C 头文件dts/<arch>/— 架构特定 DTS(如dts/arm/)dts/common/— 通用 DTSdts/bindings/— YAML binding 文件
DTS 文件按优先级收集(dts.cmake):
| 序号 | 文件来源 | 说明 |
|---|---|---|
| 1 | Board DTS 主文件(<board>.dts) |
硬件基础定义,最高优先级的基础 |
| 2 | Board 扩展 DTS | 额外板级定义 |
| 3 | Shield overlay 文件 | 扩展板硬件 |
| 4 | 应用 overlay | 开发者自定义覆盖 |
| 5 | 额外 overlay(命令行) | -DDTC_OVERLAY_FILE,最高优先级 |
应用 overlay 的自动发现顺序:
<app>/boards/socs/目录下的 SoC 特定 overlay<app>/boards/目录下的板级特定 overlay<app>/目录下任意.overlay文件<app>/app.overlay
Board DTS 本身也有 include 层级链,以 STM32H7 为例:
1 | <board>.dts ← Board DTS(入口) |
越底层的 .dtsi 越通用,越上层的 .dts 越具体。
4.3.3 阶段 2: C 预处理器合并
DTS 文件中大量使用 C 预处理器指令,必须先用交叉编译器的预处理器展开:
1 |
|
预处理命令(简化):
1 | <cross>-gcc \ |
-include 的合并效果:多个文件通过 -include 传入,等效于在虚拟文件顶部按顺序拼接。Overlay 中 &usart1 这样的标签引用,因为 Board DTS 先被 -include,标签已存在于符号表中,所以能正确解析。
输出:build/zephyr/zephyr.dts.pre — 所有 #include 和宏展开后的纯 DTS 文本。
4.3.4 阶段 3: EDT 解析与 Binding 匹配
gen_edt.py 使用 Python 库 edtlib 解析预处理后的 DTS:
1 | # zephyr/scripts/dts/gen_edt.py |
edtlib 做了四件事:
- 解析 DTS 语法:节点、属性、
&label引用、&node { ... }覆盖 - 加载 YAML bindings:根据每个节点的
compatible属性匹配 binding 文件 - 验证属性类型:检查
reg、interrupts等属性是否符合 binding 中的类型定义 - 合并 overlay 覆盖:处理
&node { status = "okay"; }等覆盖操作
Binding 发现机制:
- Zephyr 扫描所有
<DTS_ROOT>/dts/bindings/**/*.yaml - Binding 文件名必须与
compatible字符串匹配(vendor,model→vendor,model.yaml) - 应用目录下创建
dts/bindings/子目录即可添加自定义 binding - 没有 matching binding 的节点,
DT_PROP()宏无法访问其自定义属性
完整示例:从 DTS 到驱动的自定义设备
以一个自定义 I2C 温度传感器为例,展示 DTS → binding → 驱动 → API 的完整链路。
Step 1:DTS 中定义设备节点
1 | /* app.overlay — 声明硬件 */ |
Step 2:编写 binding YAML
放在 <app_dir>/dts/bindings/sensor/acme,tmp100.yaml:
1 | description: ACME TMP100 I2C temperature sensor |
Step 3:编写驱动
1 | /* drivers/sensor/acme_tmp100.c — 关键框架 */ |
Step 4:应用代码使用
1 | /* 应用代码 — 通过标准传感器 API 访问 */ |
完整的数据流转:
1 | DTS 节点 (compatible="acme,tmp100", status="okay") |
关键宏的作用:
DT_DRV_COMPAT— 声明驱动匹配哪个 compatible(acme,tmp100→ C 标识符acme_tmp100)DT_INST_FOREACH_STATUS_OKAY— 为每个匹配且 status=okay 的 DTS 节点展开一次初始化代码DT_INST_PROP(inst, prop)— 获取第 inst 个实例的属性值(如sampling_rate= 10)DEVICE_DT_INST_DEFINE— 将设备注册到内核,绑定 API 函数表DEVICE_DT_GET— 应用代码通过 DTS 标签获取设备句柄
输出:
build/zephyr/zephyr.dts— 合并后的完整设备树(调试时打开查看最终结果)build/zephyr/edt.pickle— EDT 对象的 Python pickle 序列化
4.3.5 阶段 4: C 宏生成
gen_defines.py 读取 edt.pickle,为每个 DTS 节点生成 C 预处理器宏:
1 | /* build/zephyr/include/generated/zephyr/devicetree_generated.h */ |
这些宏让 C 代码在编译时就能获取硬件信息,无需运行时查找。应用代码通过 <zephyr/devicetree.h> 提供的 DT_NODELABEL()、DT_PROP()、DT_IRQ() 等高层宏间接使用这些底层定义。
4.3.6 阶段 5: dtc 检查(可选)
1 | # dts.cmake — 如果系统安装了 dtc(Device Tree Compiler) |
dtc 在 Zephyr 中仅做语法和一致性检查,输出被完全丢弃。实际的 DTS 解析、合并和 C 宏生成由 Python 工具链(edtlib + gen_defines.py)完成。
4.3.7 DeviceTree 核心概念
理解了处理流程后,以下是从 DTS 到驱动的关键概念:
compatible — 驱动匹配的钥匙
1 | compatible = "st,stm32h7-dma", "st,stm32-dma-v2"; |
- 格式:
"vendor,model"(全小写,逗号分隔) - 多值按 specific → generic 顺序排列
- 每个 compatible 独立匹配驱动,不是“第一个匹配就停止”
- Zephyr 依靠 Kconfig 的
depends on确保只有一个驱动被编译 - 没有
compatible的节点无法匹配驱动,但可以被&label引用和DT_PROP()访问
驱动模型匹配链 — DTS 如何绑定到驱动:
flowchart LR
A["DTS 节点
compatible + status=okay"] --> B["gen_defines.py
分配 instance ID"]
B --> C["驱动源码
DT_DRV_COMPAT 匹配"]
C --> D["DT_INST_FOREACH_STATUS_OKAY
为每个实例展开代码"]
D --> E["DEVICE_DT_INST_DEFINE
注册设备 + API 函数表"]
E --> F["应用代码
DEVICE_DT_GET()
+ 子系统 API"]
style A fill:#bbdefb,stroke:#1565c0,color:#000
style E fill:#c8e6c9,stroke:#2e7d32,color:#000
style F fill:#ffe0b2,stroke:#ef6c00,color:#000
关键理解:匹配是编译时完成的。宏展开后,驱动代码为每个 DTS 中 status = "okay" 的节点生成一个设备实例,注册到内核设备列表中。没有运行时的设备名查找。
reg — 寄存器地址映射
reg 属性的格式由父节点的属性决定:
| 父节点属性 | 作用 |
|---|---|
#address-cells |
地址占几个 32-bit cell |
#size-cells |
大小占几个 32-bit cell |
bus(binding 中定义) |
决定地址 cell 的含义 |
典型格式对比:
1 | /* 内存映射总线:reg = <地址 大小> */ |
节点名中的 @address 是给人看的标识符,reg 是给驱动用的数据。DTS 规范要求两者匹配。
interrupts — 中断配置
中断格式由 interrupt-parent 指向的中断控制器 binding 决定。以 ARM Cortex-M 的 NVIC 为例:
1 | # zephyr/dts/bindings/interrupt-controller/arm,v7m-nvic.yaml |
1 | dma1: dma@40020000 { |
多个中断拼接为数组,构建工具按 #interrupt-cells 的值自动分割。
pinctrl — 引脚复用配置
STM32 的 pinctrl 分两层 binding:
pinctrl-device.yaml— 定义pinctrl-0为 phandle 类型st,stm32-pinctrl.yaml— 通过 child-binding 定义 pin 配置节点格式
1 | usart1_tx_default: usart1_tx_default { |
STM32_PINMUX('A', 9, AF7) 经 C 预处理器展开为一个 32-bit 整数,编码为:
- bit[13:9] = port(A=0, B=1, …)
- bit[8:5] = line(0-15)
- bit[4:0] = mode(AF0-AF15 或 GPIO 模式)
chosen — 系统级节点选择
Zephyr 通过 chosen 属性将 DTS 节点映射到系统功能:
1 | chosen { |
注意 zephyr,flash 和 zephyr,flash-controller 是独立的:
zephyr,flash→ 代码存在哪(决定链接脚本的 FLASH 区域 ORIGIN/LENGTH)zephyr,flash-controller→ 用哪个设备执行 Flash 操作(影响 flash API)
Label 与 phandle
1 | usart1: serial@40011000 { ... }; |
- 节点名 (
serial@40011000):硬件描述,DTS 规范要求用@address区分同类型节点 - Label (
usart1:):人类可读别名,用于&usart1跨文件引用和DT_NODELABEL()宏 - phandle:构建系统自动生成的数字 ID,只有被
&label引用的节点才会分配。在zephyr.dts中可见(如phandle = <0x3>),但永远不需要手动编写
一个节点可以有多个 label,但只能在节点定义时声明,overlay 中无法追加:
1 | /* 两个 label 都在节点定义时给出(通常在同一个 dtsi 中) */ |
4.4 链接脚本生成
链接脚本定义了代码和数据在物理内存中的布局。Zephyr 的链接脚本模板经过 C 预处理,可以动态适配不同硬件的内存配置。
4.4.1 模板选择
选择优先级:应用自定义 > 板级 > SoC > 架构默认。
1 | SOC CMakeLists.txt → 设置 SOC_LINKER_SCRIPT |
对于 ARM Cortex-M,默认模板位于:zephyr/include/zephyr/arch/arm/cortex_m/scripts/linker.ld
4.4.2 C 预处理
flowchart LR
A["linker.ld
链接脚本模板"] --> B["交叉编译器 -E
C 预处理器"]
C["autoconf.h
CONFIG_* 宏"] --> B
D["devicetree_generated.h
DT_* 宏"] --> B
B --> E["linker_zephyr_pre0.cmd
纯 ld 语法"]
style A fill:#bbdefb,stroke:#1565c0,color:#000
style E fill:#c8e6c9,stroke:#2e7d32,color:#000
预处理命令:
1 | <cross>-gcc -x assembler-with-cpp \ |
GNU ld 本身不支持 #include,但链接脚本先经过 C 预处理器,因此:
#include指令被正常展开CONFIG_SRAM_BASE_ADDRESS等 Kconfig 宏被替换为实际值DT_REG_ADDR(node)等 DeviceTree 宏被替换为实际地址LINKER_DT_REGIONS()展开为 DeviceTree 中定义的所有内存区域
4.4.3 预处理后的链接脚本
1 | /* linker_zephyr_pre0.cmd — 预处理后的关键部分 */ |
额外的 MEMORY 区域来自 DeviceTree 中的 zephyr,memory-region 节点:
1 | sdram2: sdram@d0000000 { |
zephyr,memory-attr 的位域布局(定义在 zephyr/include/zephyr/dt-bindings/memory-attr/):
| 位段 | 范围 | 用途 | 示例 |
|---|---|---|---|
| bit[15:0] | 通用属性 | cacheable, DMA 等 | DT_ATTR_DMA_READY |
| bit[19:16] | 软件自定义 | 应用层定义 | — |
| bit[31:20] | 架构特定 | ARM MPU 属性 | DT_MEM_ARM(ATTR_MPU_RAM) |
0x100000 即 DT_MEM_ARM(ATTR_MPU_RAM),表示普通 RAM 的 MPU 属性。
4.4.4 从 linker_zephyr_pre0.cmd 到最终 linker.cmd
前面介绍的是 CMake 配置阶段生成的 linker_zephyr_pre0.cmd。在实际构建中,链接脚本会经历多次预处理,每次从同一个 linker.ld 模板出发,但传入不同的宏定义:
1 | CMake 配置阶段: |
核心机制:configure_linker_script() 宏(cmake/linker/ld/target.cmake)对同一个 linker.ld 模板执行 GCC 预处理,通过不同的 -D 宏控制条件分支:
1 | # zephyr/CMakeLists.txt:1704-1713 |
预处理命令对比:
1 | # 第 1 次(CMake 配置阶段) |
两份脚本的具体差异:linker.ld 模板中有条件判断控制 .intList 段的处理方式:
1 | /* linker.ld 模板中的条件逻辑(简化)*/ |
实际对比两份预处理产物,只有这一处区别:
1 | /* linker_zephyr_pre0.cmd — 第 1 次链接用 */ |
如果项目没有启用 CONFIG_GEN_ISR_TABLES(不需要两次链接),则 generated_kernel_files 为空,构建系统直接将 zephyr_pre0.elf 作为最终产物,不会生成 linker.cmd,也跳过第二次链接。
5. Ninja 构建阶段
CMake 配置完成后,输出 build.ninja 文件。Ninja 根据其中的规则并行执行编译和链接。
5.1 源文件编译
Ninja 自动检测 CPU 核心数,并行调用交叉编译器:
1 | # C 文件编译(简化) |
每个 .c/.cpp 文件独立编译为 .obj(目标文件),然后进入链接阶段。
5.2 两次链接机制
Zephyr 使用两次链接生成最终 ELF,这是由 ARM Cortex-M 中断处理架构的特殊需求决定的。
为什么需要两次链接?
ARM Cortex-M 的硬件向量表(Vector Table)只存储函数指针,无法传递额外参数。但 Zephyr 驱动需要在中断处理函数中访问设备上下文(device context)。解决方案是引入软件 ISR 表作为间接层:
1 | 硬件中断 → 向量表中的通用入口 _isr_wrapper |
sequenceDiagram
participant Compile as 编译阶段
participant Link1 as 第 1 次链接
participant Gen as gen_isr_tables.py
participant Link2 as 第 2 次链接
Note over Compile: 每个驱动调用
IRQ_CONNECT(irq, pri, handler, dev, flags)
→ 展开为 Z_ISR_DECLARE()
→ 生成 struct _isr_list 放入 .intList 段
Compile->>Link1: 所有 .obj 的 .intList 段合并
Note over Link1: zephyr_pre0.elf
(ISR 描述信息已合并但地址未最终确定)
Link1->>Gen: 从 pre0.elf 提取 .intList 段
Note over Gen: 解析每个 struct _isr_list
获取 {irq, pri, handler, arg}
生成 isr_tables.c
Gen->>Link2: isr_tables.c 编译后参与链接
Note over Link2: zephyr.elf
(包含完整的软件 ISR 表)
详细过程:
编译阶段 — 每个驱动通过 IRQ_CONNECT 注册中断处理:
1 | // 驱动初始化代码 |
第 1 次链接 (zephyr_pre0.elf):
- 合并所有
.obj的.intList段 - 此时所有符号地址已确定
ISR 表生成 (gen_isr_tables.py):
- 从
zephyr_pre0.elf读取.intList段的二进制数据 - 解析每个
struct _isr_list条目 - 生成
isr_tables.c,包含排序后的软件 ISR 表
第 2 次链接 (zephyr.elf):
isr_tables.c被编译并参与最终链接- 最终 ELF 包含完整的软件 ISR 表和所有设备上下文
为什么不一次搞定?
DEVICE_GET(dev)获取的设备结构体地址,只有在所有目标文件链接后才能确定。第 1 次链接确定所有地址后,gen_isr_tables.py才能生成正确的 ISR 表,第 2 次链接将其纳入最终产物。
触发条件:
CONFIG_GEN_ISR_TABLES=y时启用两次链接。大多数 ARM Cortex-M 项目默认启用。
6. 最终产物
编译完成后,build/zephyr/ 目录下的关键产物:
| 文件 | 格式 | 用途 |
|---|---|---|
zephyr.elf |
ELF | 完整可执行文件(含符号表,用于调试) |
zephyr.bin |
Raw Binary | 纯二进制(用于 west flash 烧录) |
zephyr.hex |
Intel HEX | 带地址信息的烧录格式 |
zephyr.map |
Text | 链接映射文件(每个符号的地址和大小) |
调试用中间产物:
| 文件 | 说明 |
|---|---|
zephyr/.config |
Kconfig 合并后的最终配置 |
zephyr/zephyr.dts |
DeviceTree 合并后的最终设备树 |
zephyr/include/generated/autoconf.h |
CONFIG_* 宏定义 |
zephyr/include/generated/devicetree_generated.h |
DT_* 宏定义 |
linker_zephyr_pre0.cmd |
预处理后的链接脚本 |
典型内存布局
1 | 0x08000000 ┌──────────────────────┐ FLASH |
FLASH 和 RAM 的地址、大小由 chosen 节点(zephyr,flash、zephyr,sram)指向的设备节点的 reg 属性决定。额外的内存区域由 compatible = "zephyr,memory-region" 的节点自动添加到链接脚本。
7. 总结
回顾整个编译流程,数据在工具之间这样流转:
flowchart LR
subgraph 输入
A["CMakeLists.txt"]
B["prj.conf"]
C["*.dts / *.overlay"]
D["*.c / *.cpp"]
end
subgraph CMake["CMake 配置阶段"]
E["Kconfig 合并
→ autoconf.h"]
F["DeviceTree 解析
→ devicetree_generated.h"]
G["链接脚本预处理
→ linker.cmd"]
end
subgraph Ninja["Ninja 构建阶段"]
H["源文件编译
→ .obj"]
I["第 1 次链接
→ pre0.elf"]
J["ISR 表生成
→ isr_tables.c"]
K["第 2 次链接
→ zephyr.elf"]
end
A --> CMake
B --> E
C --> F
D --> H
E --> G & H
F --> G & H
G --> I
H --> I
I --> J --> K
style 输入 fill:#e3f2fd,stroke:#1565c0,color:#000
style CMake fill:#e8f5e9,stroke:#2e7d32,color:#000
style Ninja fill:#fff3e0,stroke:#ef6c00,color:#000
五个关键要点:
west build 两步走:
_run_cmake()生成build.ninja,_run_build()通过cmake --build自动路由到 Ninja。DEFAULT_CMAKE_GENERATOR = 'Ninja'硬编码在zcmake.py中。Kconfig 管软件配置:多个配置源按优先级合并,
Kconfiglib处理depends on/select等依赖关系,最终输出autoconf.h供 C 代码和链接脚本使用。增量构建通过 MD5 校验跳过无变化的重新生成。DeviceTree 管硬件描述:
.dts+.overlay经过 C 预处理器合并 → edtlib 解析并匹配 YAML binding → gen_defines.py 生成 C 宏。compatible属性是驱动匹配的核心,整个匹配在编译时通过宏展开完成。链接脚本经过 C 预处理:
.ld模板可以包含CONFIG_*和DT_*宏,由交叉编译器预处理展开为纯 ld 语法。zephyr,memory-region节点自动映射为 MEMORY 区域。两次链接解决中断分发:Cortex-M 硬件向量表无法传递设备上下文,Zephyr 引入软件 ISR 表。第 1 次链接收集 ISR 描述信息,Python 脚本生成 ISR 表后,第 2 次链接产出最终 ELF。
参考源码(基于 Zephyr 主线):
zephyr/scripts/west_commands/build.py— west build 入口zephyr/scripts/west_commands/zcmake.py— CMake 调用封装zephyr/cmake/modules/kconfig.cmake— Kconfig 处理zephyr/cmake/modules/dts.cmake— DeviceTree 处理zephyr/scripts/dts/gen_edt.py— EDT 解析zephyr/scripts/dts/gen_defines.py— C 宏生成zephyr/scripts/build/gen_isr_tables.py— ISR 表生成zephyr/include/zephyr/arch/arm/cortex_m/scripts/linker.ld— ARM Cortex-M 链接脚本模板