摘要:本文是 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-oappLinker 的事情被隐藏了。
但 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=0x40003000Devicetree:
uart0:uart@40001000{reg=<0x400010000x1000>;};Driver:
# define UART_BASE 0x40001000而:
.text .data .bss .stack这些才是真正由 Linker 控制的 ELF sections。
4. Linker 最重要的三个概念
学习 Zephyr Linker 时,你必须先掌握以下三个概念:
MEMORY SECTIONS SYMBOLS5. 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 ↓ SRAM7. 为什么 .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 → SRAM8. .bss 又不同
例如:
static int buffer[1024];如果没有显式初始化:
static int buffer[1024];它通常进入:
.bssFlash 不需要保存 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