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 与 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 之间的搬运、栈堆边界、特殊变量的绝对定位,全部由这份脚本决定。它是嵌入式项目从“能编译”走向“可部署、可维护”的关键一环。