4. 链接脚本(Linker Script)详解:内存布局、段、语法与启动配合
说实话,很多嵌入式工程师写了几年代码,都没认真看过链接脚本。我刚开始带项目时也是这样——直到有一次芯片死活跑不起来,折腾了两天,最后发现是 .bss 段没清零。嗯,从那以后我再也不敢跳过链接脚本了。
链接脚本,说白了就是告诉链接器:你的芯片内存长什么样,代码该放哪,数据该放哪。它决定了你编译出来的 .bin 或 .hex 文件能不能在目标芯片上正确运行。
4.1 内存布局:芯片的“地皮规划”
每个芯片都有自己固定的内存地址空间。比如 STM32F103,Flash 从 0x08000000 开始,SRAM 从 0x20000000 开始。链接脚本的第一件事,就是把这些“地皮”划清楚。
核心概念:链接脚本中的 MEMORY 命令用来定义芯片的物理内存区域,包括起始地址、大小和属性(只读/读写/可执行)。
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
这里 (rx) 表示 Flash 是只读、可执行的;(rwx) 表示 SRAM 可读写、可执行。我个人习惯把 Flash 叫 ROM 区,SRAM 叫 RAM 区,这样更直观。
你想想看,如果芯片的 Flash 只有 256K,你却在链接脚本里写了 512K,链接器不会报错,但烧进去的代码跑到超出地址范围就会死机。我曾经见过一个同事,把 Flash 大小写错了,产品量产了 1000 片才发现问题……
4.2 段(Sections):.text, .data, .bss
编译出来的目标文件里,代码和数据是分门别类存放的。这些类别就叫“段”。最常见的三个段是:
| 段名 | 内容 | 存放位置 | 特点 |
|---|---|---|---|
| .text | 程序代码、常量、字符串 | Flash(只读) | 掉电不丢失 |
| .data | 已初始化的全局变量、静态变量 | 运行时在 RAM,初始值在 Flash | 需要从 Flash 拷贝到 RAM |
| .bss | 未初始化或初始化为 0 的全局变量 | RAM | 启动时需清零 |
这里有个容易踩的坑:.data 段在 Flash 里存了一份初始值,芯片上电后,启动代码要把这些值从 Flash 拷贝到 RAM。如果拷贝逻辑写错了,全局变量初始值就不对。我遇到过最离谱的一次,是拷贝的源地址和目标地址写反了,结果 Flash 里的代码被覆盖了……
避坑指南:.bss 段不清零,全局变量初始值就是随机的。我曾经排查过一个电机控制器的偶发抖动问题,查了三天,最后发现是 .bss 段清零代码被优化掉了。链接脚本里一定要给 .bss 段留出足够的空间,启动文件里一定要执行清零。
4.3 链接脚本语法:SECTIONS 命令详解
SECTIONS 是链接脚本的核心,它定义了每个段怎么放、放哪里。来看一个典型的写法:
SECTIONS
{
. = ORIGIN(FLASH);
.text :
{
*(.isr_vector) /* 中断向量表 */
*(.text) /* 程序代码 */
*(.rodata) /* 只读数据 */
_etext = .; /* .text 结束地址 */
} > FLASH
. = ORIGIN(SRAM);
.data : AT(_etext)
{
_sdata = .; /* .data 起始地址 */
*(.data) /* 已初始化数据 */
_edata = .; /* .data 结束地址 */
} > SRAM
.bss :
{
_sbss = .; /* .bss 起始地址 */
*(.bss) /* 未初始化数据 */
_ebss = .; /* .bss 结束地址 */
} > SRAM
}
注意 .data : AT(_etext) 这一行。AT 表示加载地址(在 Flash 里),> SRAM 表示运行地址(在 RAM 里)。这就是为什么 .data 段需要拷贝——它有两个地址。
我个人习惯在每个段前后都定义符号(如 _sdata、_edata),这样启动文件里可以直接用这些符号做拷贝和清零。你想想看,如果符号名写错了,链接器不会报错,但启动代码就找不到正确的地址了。
4.4 启动文件与链接脚本的配合
启动文件(startup_xxx.s)是芯片上电后执行的第一段代码。它和链接脚本是“搭档”关系——链接脚本定义内存布局,启动文件执行初始化操作。
典型的启动流程是这样的:
- 设置栈指针 SP(从链接脚本定义的 _estack 获取)
- 调用 SystemInit() 初始化时钟
- 拷贝 .data 段:从 _etext 拷贝到 _sdata,长度 _edata - _sdata
- 清零 .bss 段:从 _sbss 到 _ebss 全部写 0
- 跳转到 main() 函数
来看一段启动文件中的拷贝代码(ARM Cortex-M 汇编):
ldr r0, =_etext /* 源地址:Flash 中的 .data 初始值 */
ldr r1, =_sdata /* 目标地址:RAM 中的 .data 起始 */
ldr r2, =_edata /* 结束地址 */
subs r2, r1 /* 计算长度 */
beq .L_clear_bss /* 如果长度为 0,跳过 */
.L_copy_data:
ldrb r3, [r0], #1 /* 从 Flash 读一个字节 */
strb r3, [r1], #1 /* 写入 RAM */
subs r2, #1
bne .L_copy_data
提示:链接脚本里定义的符号(如 _etext、_sdata)在汇编代码中可以直接使用。但要注意,这些符号是地址值,不是变量。在 C 代码中引用时要用 extern uint32_t _etext; 然后取地址 &_etext。
这里有个常见问题:如果链接脚本里 .data 段的加载地址和运行地址没配对,启动文件拷贝的数据就是错的。我建议你在调试时,先打印出 _etext、_sdata、_edata 的值,确认它们符合预期。
4.5 知识结构总览
下面这张图把链接脚本、段、启动文件的关系串起来了:
从这张图可以清楚看到:链接脚本负责“画地图”,启动文件负责“按地图施工”。两者必须严格配合,任何一个符号名写错、地址算错,芯片就跑不起来。
4.6 常见问题与避坑
- .data 段拷贝后变量值不对——检查 AT 地址是否指向正确的 Flash 位置,我曾经把 AT 地址写成了 0,结果从 Flash 开头拷贝了一堆中断向量表到 RAM……
- .bss 段没清零导致全局变量随机——确认启动文件中清零循环的起始地址和结束地址正确,注意符号是地址值,不是变量值
- 链接时报错“region overflow”——代码或数据超出了芯片的 Flash/SRAM 大小,检查链接脚本中的 LENGTH 是否写对了
- 中断向量表没放在开头——芯片复位后会从 Flash 起始地址读取栈指针和复位向量,.isr_vector 段必须放在第一个
调试技巧:在启动文件中加一段代码,把 _sdata、_edata、_sbss、_ebss 的值通过串口打印出来。如果这些值和你预期的不一样,问题大概率出在链接脚本上。我每次移植新芯片都会这么做,能省下大量排查时间。