3、链接脚本精讲:链接脚本(.ld)结构解析、内存布局定义、段(text/data/bss)的映射规则
好,咱们进入第三讲。链接脚本,很多人觉得它神秘,甚至有点吓人。其实说白了,它就是一份给链接器的“地图”和“施工图”。你告诉链接器:“芯片里有哪些内存区域,我的代码和数据分别该放哪儿。” 就这么简单。
我刚开始做Bootloader那会儿,也踩过不少坑。有一次,APP跳转后死活跑不起来,查了三天,最后发现是链接脚本里一个地址写错了。嗯,从那以后,我对.ld文件就格外敬畏。
3.1 链接脚本的结构解析
一个标准的.ld文件,通常由三大部分组成:内存布局定义、段映射规则、以及符号定义。咱们一个一个来看。
3.1.1 内存布局定义(MEMORY命令)
MEMORY命令,就是告诉链接器:“嘿,我的芯片里有哪些可用的存储区域,它们的起始地址和大小是多少。” 我个人习惯把这段放在脚本最前面,一目了然。
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
这里要注意几点:
- ORIGIN:起始地址。对于STM32来说,Flash通常从0x08000000开始,RAM从0x20000000开始。但不同型号有差异,一定要查手册。
- LENGTH:大小。别写大了,否则链接器会报错。
- 属性:r=读,w=写,x=执行。Flash一般给rx,RAM给rwx。
3.1.2 段映射规则(SECTIONS命令)
SECTIONS命令,是链接脚本的核心。它定义了每个段(.text, .data, .bss等)该放到哪个内存区域,以及它们的排列顺序。
一个典型的SECTIONS结构长这样:
SECTIONS
{
.text :
{
*(.text) /* 所有输入文件的.text段 */
*(.rodata) /* 只读数据 */
_etext = .; /* 定义符号,标记text段结束 */
} > FLASH
.data : AT (ADDR(.text) + SIZEOF(.text))
{
_sdata = .; /* data段起始地址(在RAM中) */
*(.data)
_edata = .; /* data段结束地址 */
} > RAM
.bss :
{
_sbss = .;
*(.bss)
_ebss = .;
} > RAM
}
这里有个关键点:.data段为什么需要AT? 你想想看,.data段里的变量是有初始值的,比如 int a = 100;。这个初始值100,必须存放在Flash里,否则一断电就丢了。但运行时,这些变量又必须在RAM里,因为Flash不能写。所以,链接器会把.data段的初始值放在Flash里(通过AT指定加载地址),然后在启动代码里,把它们从Flash拷贝到RAM。
核心概念: VMA(虚拟地址,运行时地址)和LMA(加载地址,存储地址)。对于.text段,VMA和LMA都在Flash。对于.data段,VMA在RAM,LMA在Flash。这个区别,是理解链接脚本的关键。
3.2 内存布局的实战考量
在实际项目中,内存布局不是随便写的。我见过不少新手,直接把默认的链接脚本拿来用,结果Bootloader和APP的地址重叠了,跳转后直接跑飞。
3.2.1 Bootloader的内存布局
假设你的Bootloader占32K,APP占剩下的Flash。那么Bootloader的链接脚本应该这样:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
而APP的链接脚本,Flash起始地址要偏移:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 480K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
为什么是0x08008000?因为32K = 0x8000。这个计算要仔细,我曾经因为算错偏移量,导致APP的向量表指向了Bootloader的区域,一进中断就死循环。
3.2.2 向量表的重定位
这里要提一个重要的符号:_estack。它定义了栈顶地址,通常放在RAM的最高地址。在启动文件里,第一件事就是设置栈指针SP为_estack,然后跳转到Reset_Handler。
对于APP来说,除了链接脚本要改,还要在代码里重定位向量表:
SCB->VTOR = 0x08008000; // 告诉CPU,APP的向量表在这里
这一步不做,APP的中断一个都进不去。嗯,这里要注意,很多芯片的VTOR寄存器需要按128字节对齐。
3.3 段映射的细节与技巧
咱们再深入一点,看看每个段的具体映射规则。
3.3.1 .text段:代码和只读数据
.text段包含你的程序指令和常量。它必须放在可执行的区域,通常是Flash。链接器会按照你在SECTIONS里写的顺序,把各个输入文件的.text段拼接起来。
有时候,你需要把某些函数放在固定的地址,比如中断向量表。这时可以用KEEP关键字:
.isr_vector :
{
KEEP(*(.isr_vector)) // 强制保留,不被优化掉
} > FLASH
3.3.2 .data段:已初始化的全局变量
.data段在Flash里存的是初始值,在RAM里是运行时变量。启动代码必须负责拷贝。我建议在链接脚本里定义好起始和结束符号,方便C代码引用:
extern uint32_t _sdata, _edata, _sidata; // _sidata是Flash中的加载地址
然后在启动文件里:
for (src = &_sidata, dst = &_sdata; dst < &_edata; ) {
*dst++ = *src++;
}
3.3.3 .bss段:未初始化的全局变量
.bss段在RAM里,不需要在Flash里占空间。启动代码只需要把它清零即可:
for (dst = &_sbss; dst < &_ebss; ) {
*dst++ = 0;
}
小技巧: 如果你的芯片RAM很小,可以考虑把一些大的、不需要初始化的数组放到自定义段里,比如.noinit段。这样启动时不会清零,节省时间。但要注意,这些变量的初始值是随机的。
3.4 常见问题与避坑指南
最后,分享几个我实际项目中遇到的坑:
- 符号未定义:链接时提示
undefined reference to _sdata。检查链接脚本里是否定义了这些符号,以及启动文件里是否用extern声明了。 - 地址对齐:某些段需要对齐到4字节或8字节。可以用
ALIGN(4)来强制对齐。 - 段重叠:如果Bootloader和APP的链接脚本里,Flash或RAM区域有重叠,下载时会互相覆盖。一定要仔细核对地址范围。
我曾经在一个项目里,因为链接脚本里少写了一个AT,导致.data段的初始值没有被正确加载到Flash里。结果每次上电,全局变量的值都是乱的。查了整整一天,最后发现是链接脚本的问题。嗯,从那以后,我每次写完链接脚本,都会用objdump或者IDE的map文件,仔细检查每个段的地址和大小。
好了,这一讲就到这里。链接脚本虽然看起来枯燥,但它是Bootloader和APP跳转的基石。下一讲,咱们聊聊启动文件的细节,看看CPU上电后到底干了些什么。