链接脚本深入:.ld 文件如何掌控你的内存布局
说实话,很多做嵌入式的新手,第一次看到链接脚本(.ld 文件)时,都会觉得它像天书。一堆花括号、点号、大于号,看着就头疼。我当年也一样,第一次接触 STM32 的链接脚本,直接复制粘贴就用了,结果程序跑飞了都不知道为什么。
后来我才明白,链接脚本其实就是告诉链接器:你的芯片内存长什么样,代码该放哪,数据该放哪。说白了,它就是一张内存地图。今天我们就把它彻底讲透。
链接脚本到底在干什么?
你写好的 C 代码,经过编译,变成了一个个目标文件(.o 文件)。这些目标文件里,有代码段(.text)、已初始化数据段(.data)、未初始化数据段(.bss)等等。链接器的工作,就是把这些散落的段,按照链接脚本的指示,安排到芯片内存的指定位置。
我习惯把链接脚本比作一个「搬家公司的调度员」。你有一堆家具(代码和数据),调度员告诉你:沙发放客厅,床放卧室,书桌放书房。链接脚本就是那个调度员,它告诉链接器:.text 放 Flash,.data 放 RAM,.bss 也放 RAM。
核心作用总结:
- 定义芯片的内存布局(Flash 从哪开始,RAM 从哪开始)
- 指定各个段的存放位置(代码放 Flash,变量放 RAM)
- 提供符号地址(比如 _sdata、_ebss,供启动代码使用)
内存布局定义:你得先告诉链接器你的芯片长什么样
每个芯片的内存映射都不一样。比如 STM32F103C8T6,Flash 从 0x08000000 开始,大小 64KB;RAM 从 0x20000000 开始,大小 20KB。这些信息必须在链接脚本里先声明。
看一个典型的声明:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
}
这里我解释一下:FLASH (rx) 表示这块内存是只读(r)且可执行(x)的。RAM (xrw) 表示可读可写可执行。ORIGIN 是起始地址,LENGTH 是大小。
嗯,这里要注意:LENGTH 的单位是 K 或 M,别写错了。我曾经见过有人把 64K 写成 64,结果链接器只分配了 64 字节,程序直接跑飞。
段的分配规则:.text、.data、.bss 各归其位
有了内存区域,接下来就是分配段了。这是链接脚本的核心部分,也是最容易出坑的地方。
.text 段:代码和只读数据
.text 段包含你的程序指令、常量字符串、函数表等。它必须放在 Flash 里,因为 Flash 是非易失性的,掉电不丢失。
SECTIONS
{
.text :
{
*(.text) /* 所有目标文件的 .text 段 */
*(.text.*) /* 所有 .text.xxx 子段 */
*(.rodata) /* 只读数据,比如 const 变量 */
*(.rodata.*)
_etext = .; /* 标记 .text 结束地址 */
} > FLASH
}
注意那个 _etext = .,这里的 . 表示当前地址计数器。这行代码把 .text 段的结束地址保存到一个符号里,启动代码后面会用到它。
我的经验: 如果你用了 C++ 的全局对象构造函数,或者用了 __attribute__((section(".text.init"))),记得在 .text 段里加上对应的输入段。不然链接器会报 undefined reference 错误。
.data 段:已初始化的全局变量
.data 段存放的是你定义时赋了初值的全局变量和静态变量。比如 int a = 10;,这个 10 就存在 .data 段里。
但有个问题:.data 段的内容在程序运行前必须存在,而 Flash 是只读的,RAM 是可读写的。所以实际做法是:.data 段在 Flash 里存储初始值,启动时由 bootloader 或 CRT 代码复制到 RAM 中。
.data : AT (ADDR(.text) + SIZEOF(.text))
{
_sdata = .; /* .data 起始地址(RAM 中) */
*(.data)
*(.data.*)
_edata = .; /* .data 结束地址(RAM 中) */
} > RAM AT > FLASH
这里有个关键语法:> RAM AT > FLASH。意思是:运行时放在 RAM 里,但加载地址在 Flash 里。链接器会自动计算 Flash 中 .data 的存储位置,并生成符号供启动代码使用。
我曾经踩过的坑: 有一次我忘了写 AT > FLASH,结果链接器把 .data 直接放在 RAM 的起始地址,但 RAM 掉电就丢数据。上电后 .data 里的初始值全是随机数,程序直接崩溃。这个坑我花了整整一天才找到。
.bss 段:未初始化的全局变量
.bss 段存放的是未初始化或初始化为 0 的全局变量。比如 int b; 或 int c = 0;。这些变量在 Flash 里不需要占用空间,只需要在 RAM 里预留位置,然后在启动时清零。
.bss :
{
_sbss = .; /* .bss 起始地址 */
*(.bss)
*(.bss.*)
*(COMMON) /* 通用符号 */
_ebss = .; /* .bss 结束地址 */
} > RAM
注意 .bss 没有 AT > FLASH,因为它不需要从 Flash 复制数据,只需要在 RAM 里清零即可。
完整的链接脚本示例
把上面几段拼起来,就是一个完整的链接脚本了。我贴一个我常用的模板:
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
}
SECTIONS
{
.text :
{
*(.isr_vector) /* 中断向量表 */
*(.text)
*(.text.*)
*(.rodata)
*(.rodata.*)
_etext = .;
} > FLASH
.data : AT (ADDR(.text) + SIZEOF(.text))
{
_sdata = .;
*(.data)
*(.data.*)
_edata = .;
} > RAM
.bss :
{
_sbss = .;
*(.bss)
*(.bss.*)
*(COMMON)
_ebss = .;
} > RAM
}
这个脚本定义了中断向量表、代码段、只读数据段、已初始化数据段和未初始化数据段。启动代码会利用 _sdata、_edata、_sbss、_ebss 这些符号来完成 .data 的复制和 .bss 的清零。
链接脚本中的符号:启动代码的桥梁
你可能会问:启动代码怎么知道 .data 要复制多少字节?.bss 要从哪开始清零?答案就是链接脚本里定义的符号。
| 符号名 | 含义 | 启动代码用途 |
|---|---|---|
_sdata |
.data 段在 RAM 中的起始地址 | 复制目标地址 |
_edata |
.data 段在 RAM 中的结束地址 | 计算复制长度 |
_etext |
.text 段结束地址(即 .data 在 Flash 中的存储位置) | 复制源地址 |
_sbss |
.bss 段起始地址 | 清零起始地址 |
_ebss |
.bss 段结束地址 | 清零结束地址 |
启动代码里通常是这样用的:
extern uint32_t _sdata, _edata, _etext;
extern uint32_t _sbss, _ebss;
void Reset_Handler(void)
{
uint32_t *src, *dst;
/* 复制 .data 段 */
src = &_etext;
dst = &_sdata;
while (dst < &_edata) {
*dst++ = *src++;
}
/* 清零 .bss 段 */
dst = &_sbss;
while (dst < &_ebss) {
*dst++ = 0;
}
/* 跳转到 main */
main();
}
你看,链接脚本和启动代码就是这么配合的。链接脚本提供地址信息,启动代码执行实际操作。
用 SVG 理清整个流程
说了这么多,我画张图帮你把整个流程串起来。这张图展示了从编译到链接,再到启动时内存布局的完整过程。
常见问题与避坑指南
最后,我分享几个我在项目中实际遇到过的问题,希望能帮你少走弯路。
问题 1:.data 段复制后程序还是跑飞
我曾经在移植一个 bootloader 时,发现 .data 段复制后,全局变量还是不对。查了半天,发现是链接脚本里 AT > FLASH 写成了 AT > RAM。结果链接器把 .data 的加载地址也放在了 RAM 里,上电后 RAM 里的初始值全是垃圾。记住:.data 的加载地址必须在 Flash 里。
问题 2:中断向量表地址不对
有一次我写了一个 bootloader,需要把应用程序的中断向量表偏移到 0x08010000。结果我忘了在链接脚本里设置 VECT_TAB_OFFSET,导致中断响应全乱套了。后来我在 .text 段里显式指定了中断向量表的位置:KEEP(*(.isr_vector)),并在代码里调用了 SCB->VTOR = FLASH_BASE | 0x10000; 才解决。
问题 3:链接脚本里忘了处理 COMMON 段
如果你在多个源文件里定义了未初始化的全局变量,链接器可能会把它们放在 COMMON 段里。如果 .bss 段里没包含 *(COMMON),这些变量就不会被清零,初始值就是随机的。我建议在 .bss 段里始终加上 *(COMMON),避免这种隐晦的 bug。
好了,链接脚本的核心内容就这些。你只要理解了 MEMORY 和 SECTIONS 这两个关键字,再搞清楚 .text、.data、.bss 的分配规则,就能自己写链接脚本了。下次遇到程序跑飞,别忘了先检查一下 .ld 文件——很多时候,问题就出在这张「内存地图」上。