news 2026/9/29 9:13:43

Zephyr BSP: 33-Linker Memory Map Explanation

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Zephyr BSP: 33-Linker Memory Map Explanation

摘要:本文是 Zephyr BSP 移植系列的第 33 篇,聚焦 Linker 与 Memory Map。文章从 Linker 在 BSP 中的定位讲起,对比普通 C 程序与 MCU 的差异,逐步拆解 MEMORY、SECTIONS、SYMBOLS 三大核心概念,深入分析 .data、.bss 的特殊性,并串联 Zephyr 特有的初始化 section、device section 与 iterable sections。随后梳理 SoC、Board、Linker 三者的协作关系,讲解 .map 文件的调试方法、Flash/RAM overflow 的典型错误、XIP 与 MPU/MMU 对 Linker 的依赖,最终把 21~33 篇内容闭环为完整的 SoC BSP 平台。

Linker / Memory Map:Zephyr BSP 最后一道「硬件边界」

前面你已经走完:

21SoC Port Skeleton22CPU / Architecture23Startup24Interrupt Controller25Clock / Reset26Devicetree27Binding28UART Driver29GPIO / SPI / I2C / Timer30Board Support Package31Kconfig32CMake / Build System

到了33 — Linker / Memory Map,我们开始处理一个非常关键的问题:

Zephyr 编译出来的代码,最终到底应该被放到 Company SoC 的哪一块物理内存里?

这一步实际上把:

C/C++代码 ↓ Compiler ↓ Object files ↓ Linker ↓ ELF ↓ Flash / SRAM / ROM / XIP / RAM

真正串起来。

1. 先理解 Linker 在 BSP 里的位置

整个 Zephyr BSP 可以粗略画成:

Zephyr Application │ ▼ ┌─────────────┐ │ CMake │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ Compiler │ │ gcc/clang │ └──────┬──────┘ │ .o / .a files │ ▼ ┌─────────────┐ │ Linker │ │ ld │ └──────┬──────┘ │ ▼ ELF │ ┌────────────┴────────────┐ ▼ ▼ Memory Layout Symbols │ ▼ ┌──────────────────┐ │ Flash / ROM │ │ SRAM │ │ Stack │ │ Heap │ │ Device regions │ └──────────────────┘

所以:

CMake 决定「编译哪些东西」,Linker 决定「这些东西最终放在哪里」。

这两个概念一定不要混淆。

2. 为什么普通 C 程序感觉不到 Linker

在 PC 上:

intmain(){printf("hello");}

你通常只需要:

gcc main.c-oapp

Linker 的事情被隐藏了。

但 MCU 上则完全不同。

假设 Company SoC:

Flash 0x00000000 ───────────────── │ │512KB │ 0x00080000 ───────────────── SRAM 0x20000000 ───────────────── │ │128KB │ 0x20020000 ─────────────────

那么 Linker 必须知道:

.text → Flash .rodata → Flash .data → SRAM .bss → SRAM .stack → SRAM

这就是:
Memory Map

3. 一个真实 MCU Memory Map

假设我们的 Company SoC:

CompanySoC-X1 ──────────────────────────────── 0x0000_0000 ┌──────────────────────────────┐ │ │ │ FLASH │ │ │ │ .vector_table │ │ .text │ │ .rodata │ │ │ │512KB │ │ │ └──────────────────────────────┘ 0x0008_0000 0x2000_0000 ┌──────────────────────────────┐ │ SRAM │ │ │ │ .data │ │ .bss │ │ heap │ │ stack │ │ │ │128KB │ │ │ └──────────────────────────────┘ 0x2002_0000 0x4000_0000 ┌──────────────────────────────┐ │ Peripheral │ │ │ │ UART │ │ GPIO │ │ SPI │ │ I2C │ │ TIMER │ │ │ └──────────────────────────────┘

注意:

Peripheral 不一定是 Linker section。

它只是 CPU 的 memory-mapped address space。

例如:

UART0_BASE=0x40001000 GPIO_BASE=0x40002000 SPI0_BASE=0x40003000

Devicetree:

uart0:uart@40001000{reg=<0x400010000x1000>;};

Driver:

# define UART_BASE 0x40001000

而:

.text .data .bss .stack

这些才是真正由 Linker 控制的 ELF sections。

4. Linker 最重要的三个概念

学习 Zephyr Linker 时,你必须先掌握以下三个概念:

MEMORY SECTIONS SYMBOLS

5. MEMORY:告诉 Linker「芯片有什么内存」

最简单的 linker script:

MEMORY{FLASH(rx):ORIGIN=0x00000000, LENGTH=512K SRAM(rwx):ORIGIN=0x20000000, LENGTH=128K}

意思是:

FLASH start=0x00000000 size=512KB SRAM start=0x20000000 size=128KB

这实际上就是:
把 Company SoC 的物理 Memory Map 告诉 Linker。

6. SECTIONS:告诉 Linker「代码放哪里」

例如:

SECTIONS{.text:{*(.text*)}>FLASH .rodata:{*(.rodata*)}>FLASH .data:{*(.data*)}>SRAM .bss:{*(.bss*)}>SRAM}

下面是一个完整的 CompanySoC-X1 链接脚本示例,把前面讲的 MEMORY、SECTIONS 以及 .data 的 LMA/VMA 处理整合到一起:

/* * CompanySoC-X1 linker script * 完整示例:MEMORY + SECTIONS + .data LMA/VMA 处理 */ /* ========== 1. MEMORY:告诉 Linker 芯片有什么内存 ========== */ MEMORY { /* 片上 Flash:512 KB,起始地址 0x00000000,可读可执行 */ FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 512K /* 片上 SRAM:128 KB,起始地址 0x20000000,可读可写可执行 */ SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K } /* ========== 2. SECTIONS:告诉 Linker 代码放哪里 ========== */ SECTIONS { /* ---- 向量表:必须放在 Flash 起始位置 ---- */ .vector_table : { KEEP(*(.vector_table)) } > FLASH /* ---- 代码段:只读,放 Flash ---- */ .text : { *(.text*) } > FLASH /* ---- 只读数据:放 Flash ---- */ .rodata : { *(.rodata*) } > FLASH /* * ---- .data:有初始值的全局/静态变量 ---- * * 关键点:LMA(Load Memory Address)在 Flash, * VMA(Virtual Memory Address)在 SRAM。 * * 语法:AT> FLASH 表示"加载地址在 Flash", * > SRAM 表示"运行地址在 SRAM"。 * * 启动时 startup code 必须把这段从 Flash 拷贝到 SRAM。 */ .data : { __data_start = .; /* 运行地址起点(VMA) */ *(.data*) __data_end = .; /* 运行地址终点(VMA) */ } > SRAM AT> FLASH /* 记录 .data 在 Flash 中的加载地址(LMA),供 startup 拷贝使用 */ __data_load_start = LOADADDR(.data); __data_load_end = LOADADDR(.data) + SIZEOF(.data); /* ---- .bss:未初始化/零初始化变量,只占 SRAM,Flash 不保存 ---- */ .bss (NOLOAD) : { __bss_start = .; *(.bss*) *(COMMON) __bss_end = .; } > SRAM /* ---- 栈:放在 SRAM 末尾,向下增长 ---- */ .stack (NOLOAD) : { __stack_start = .; . = . + 4K; /* 预留 4 KB 栈空间 */ __stack_end = .; } > SRAM }

这段脚本里最值得关注的是.data的处理:

.data │ ├── LMA(加载地址)→ Flash │ 初始值123保存在 Flash │ └── VMA(运行地址)→ SRAM 启动后 counter=123在 SRAM

对应的 startup code 需要做两件事:

/* 1. 把 .data 从 Flash 拷贝到 SRAM */memcpy(&__data_start,&__data_load_start,__data_end-__data_start);/* 2. 把 .bss 清零 */memset(&__bss_start,0,__bss_end-__bss_start);

这样,int counter = 123;的初始值保存在 Flash,运行时被拷贝到 SRAM;static int buffer[1024];则直接在 SRAM 清零,不占用 Flash 空间。

意思:

.text ↓ FLASH .rodata ↓ FLASH .data ↓ SRAM .bss ↓ SRAM

7. 为什么 .data 很特殊?

例如:

int counter=123;

这是:

.data

程序启动之前:

Flash: counter initial value=123

启动以后:

SRAM: counter=123

所以 .data 有:

Load Address+Virtual/Runtime Address

可以理解成:

Flash │ │ initial value ▼ ┌────────────┐ │ .data │ └─────┬──────┘ │ copy ▼ SRAM ┌────────────┐ │ .data │ │counter=123│ └────────────┘

这也是为什么 startup code 必须做:

copy .data from FLASH → SRAM

8. .bss 又不同

例如:

static int buffer[1024];

如果没有显式初始化:

static int buffer[1024];

它通常进入:

.bss

Flash 不需要保存 4096 个字节的 0。
所以:

.bss ↓ SRAM

启动时:

memset(__bss_start,0, __bss_end - __bss_start);

于是:

.bss

被清零。

9. Zephyr 的 linker 比普通裸机复杂得多

你不能简单认为 Zephyr 就是:

.text .data .bss

实际上 Zephyr 会有大量特殊 sections,例如:

.text .rodata .data .bss .noinit .device .device_states .sw_isr_table .z_init_PRE_KERNEL_1 .z_init_PRE_KERNEL_2 .z_init_POST_KERNEL .z_init_APPLICATION .shell .log_const .log_backends .ARM.exidx

其中有一些特别重要。

10. z_init_* 和我们之前学的 Init Priority

还记得前面:

PRE_KERNEL_1 PRE_KERNEL_2 POST_KERNEL APPLICATION

我们之前从:

DEVICE_DT_DEFINE(...)

一路追到了:

device initialization

现在从 Linker 的角度重新看。

Zephyr 会把初始化函数放进 linker sections。

概念上类似:

.z_init_PRE_KERNEL_1 │ ▼ .z_init_PRE_KERNEL_2 │ ▼ .z_init_POST_KERNEL │ ▼ .z_init_APPLICATION

因此:
Zephyr 的初始化顺序不仅仅是 C 代码调用关系,也是 Linker section 布局的一部分。
这就是为什么你前面学习:

DEVICE_DT_DEFINE

最后一定会碰到 linker。

11. struct device 也和 Linker 有关系

前面我们已经拆过:

DEVICE_DT_DEFINE
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 9:11:31

MinGW64安装避坑指南:从环境变量到编译FFmpeg 4.4

看到这个标题我先笑了一下,“不会安装MinGW64,rt”,这不就是论坛伸手党的经典开场吗。但说真的,MinGW64的安装还真不是右键解压那么无脑,网上教程抄来抄去,版本、线程模型、异常处理这几个概念一混&#xf…

作者头像 李华
网站建设 2026/9/29 9:10:03

LLM推理性能调优:显存带宽、KV Cache与硬件加速器实战

这两年做LLM相关项目,最直观的感受是:模型越换越大,显卡成了硬通货。我手头一个生产环境的对话系统,从7B模型切到14B之后,推理吞吐直接掉了一半多,折腾了快两周,最后靠调整量化方案、推理引擎和…

作者头像 李华
网站建设 2026/9/29 9:09:31

软件测试必备:每天5分钟掌握SQL查询与INSERT数据操作

做软件测试,尤其是功能测试和接口测试的,早晚会遇到一个躲不开的场面:你刚提交了一个bug,开发回复“数据是正常的,你再去库里看看”。这时候你打开数据库管理工具,面对一张表,却连“查出来给我看…

作者头像 李华
网站建设 2026/9/29 9:07:58

Ubuntu下kill进程全解析:从信号机制到kill -9的正确使用姿势

最近后台好几个读者留言问同一个问题:在 Ubuntu 上跑着一个卡死的程序,前台 CtrlC 不起作用,直接关终端又怕把数据搞坏,到底该用 kill 那个参数?有人张口就是 kill -9 无脑强杀,有人连 kill 和 pkill 的区别…

作者头像 李华
网站建设 2026/9/29 9:07:39

工业控制器三合一:PLC、HMI与边缘AI融合方案解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华