4. 链接脚本与内存布局

在嵌入式项目里,链接脚本(Linker Script)是连接“编译产物”和“物理存储器”的桥梁。编译器把每个 .c 文件编译成目标文件时,并不知道代码最终会放在 Flash 的哪个地址、变量会落在 RAM 的哪个区间;这些地址分配工作全部由链接器根据链接脚本完成。SEGGER Embedded Studio 默认会为所选芯片生成一份链接脚本,但一旦项目需要自定义 Bootloader 分区、外扩 RAM、双区固件或特殊段定位,就必须理解并修改它。

链接脚本的核心语法

链接脚本使用 GNU ld 风格的脚本语言,主要由三类语句构成:MEMORY 声明物理存储区域,SECTIONS 描述输入段到输出段的映射,以及符号赋值与表达式。一个典型的 MEMORY 块如下:

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
  CCM   (rwx) : ORIGIN = 0x10000000, LENGTH = 64K
}

其中 ORIGIN 是起始地址,LENGTH 是长度,括号内的 r/w/x 表示读、写、执行权限。权限不是装饰,它会影响链接器对段的分配合法性判断:只读代码段不能放进没有 x 权限的区域。

SECTIONS 块负责把输入段(如 .text、.data、.bss)归入输出段并指定运行地址(VMA)和加载地址(LMA)。理解 VMA 与 LMA 的区别是本节的关键:.data 段的变量在运行时位于 RAM(VMA),但它的初值必须保存在 Flash 中(LMA),启动代码再把它从 Flash 拷贝到 RAM。链接脚本中常用 AT> 指定 LMA:

.data : AT(_sidata)
{
  _sdata = .;
  *(.data*)
  _edata = .;
} > RAM

.bss :
{
  _sbss = .;
  *(.bss*) *(COMMON)
  _ebss = .;
} > RAM

这里的 _sidata、_sdata、_edata、_sbss、_ebss 就是符号导出。它们本身不占存储空间,只是地址常量,供 C 代码在启动文件中引用,完成数据搬运和 BSS 清零。

链接脚本驱动的内存布局与启动搬运 FLASH 0x08000000 512K .isr_vector(中断向量表) _sidata 起始 .text(代码) 只读,x 权限 .rodata(常量) 只读 .data 初值副本(LMA) _sidata → _edata RAM 0x20000000 128K .data(VMA 运行区) _sdata → _edata .bss(未初始化) _sbss → _ebss Heap(堆) _end → __heap_end Stack(栈) __stack_start → __stack_end 向下增长,MSP 初始值 启动时拷贝 .data BSS 清零

配置 Flash 与 RAM 区域

在 Embedded Studio 中,链接脚本通常位于项目目录的 linker/ 下,文件名类似 your_project.icf(IAR 风格)或 .ld(GNU 风格),具体取决于所选工具链。修改前应先确认芯片手册中的存储器映射:Flash 起始地址、容量、RAM 起始地址、是否有 CCM/TCM 等紧耦合内存。例如 STM32F407 的 CCM RAM 位于 0x10000000,容量 64K,它不能用于 DMA 访问,但可以放栈或高频变量以提升性能。

配置时要注意三点:第一,各区域不能重叠,否则链接器会报 region overflowed;第二,LENGTH 要留出 Bootloader 或参数区占用的空间,例如 Bootloader 占 32K,则应用 Flash 应从 0x08008000 开始,长度改为 480K;第三,若使用外扩 SDRAM,需在启动代码中先初始化 FSMC/FMC 控制器,再让链接器把某些段放入该区域,否则程序会在访问未初始化内存时 HardFault。

堆栈大小设置

栈(Stack)和堆(Heap)的大小在链接脚本中通过符号定义,常见写法是:

__stack_size = 0x1000;   /* 4KB */
__heap_size  = 0x2000;   /* 8KB */

.stack (NOLOAD) :
{
  . = ALIGN(8);
  __stack_start = .;
  . += __stack_size;
  __stack_end = .;
} > RAM

.heap (NOLOAD) :
{
  . = ALIGN(8);
  __heap_start = .;
  . += __heap_size;
  __heap_end = .;
} > RAM

栈大小需要根据最坏情况估算:中断嵌套层数 × 每层保存的寄存器与局部变量,加上主循环调用深度。若使用 RTOS,每个任务还有独立栈,需在任务创建时单独指定。堆大小则取决于是否使用 malloc;裸机项目若完全不用动态分配,可把堆设为 0,把空间留给栈。判断栈是否溢出,可以在调试时观察 MSP 是否越过 __stack_end,或在栈底填充魔数(如 0xDEADBEEF)后检查是否被改写。

段定位与符号导出

除了标准段,项目常需要把特定变量放到指定地址,例如把校准参数放在 Flash 末尾、把 DMA 缓冲区放到 CCM。做法是在 C 代码中用 __attribute__((section(".my_section"))) 声明,再在链接脚本中为该段指定地址:

__attribute__((section(".calib"))) const uint32_t calib_data[16] = {...};

.calib : 
{
  KEEP(*(.calib))
} > FLASH

KEEP 防止链接器因“未引用”而丢弃该段。符号导出则通过 PROVIDE 或直接赋值完成,C 代码中用 extern uint32_t _sdata; 声明后即可取地址使用。注意符号是地址而非变量值,取用时要用 &_sdata 或直接作为指针。

修改链接脚本后,务必重新编译并查看 .map 文件,确认各段起始地址、大小和符号地址符合预期。Embedded Studio 的“Linker Map”视图可直接搜索段名和符号名,是验证内存布局最直接的手段。
不要随意删除默认链接脚本中的 .isr_vector 段或 KEEP 关键字,否则中断向量表可能被优化掉,导致复位后无法进入 Reset_Handler。修改前建议先备份原始脚本。

掌握链接脚本后,你就能精确控制固件在芯片中的落点:Bootloader 与应用分区、数据在 Flash 与 RAM 之间的搬运、栈堆边界、特殊变量的绝对定位,全部由这份脚本决定。它是嵌入式项目从“能编译”走向“可部署、可维护”的关键一环。