第3章:链接脚本深度解析——.ld语法与段布局
链接脚本,说白了就是告诉链接器:你的代码该放哪儿,数据该放哪儿。我当年刚入行时,觉得这玩意儿不就是个配置文件嘛,随便写写就行。结果呢?全局变量全乱套了——明明赋了初值的变量,上电后全是0;该在RAM里跑的数据,跑到了Flash里。那次调试花了我整整两天,最后发现是链接脚本里一个段地址写错了。
嗯,从那以后,我养成了一个习惯:每次写链接脚本,都把它当成核心设计文档来对待。今天咱们就把这玩意儿彻底讲透。
3.1 GCC链接脚本语法速览
GCC用的链接脚本,后缀是.ld。它的语法其实不复杂,核心就几个关键字:
| 关键字 | 作用 | 我常用的写法 |
|---|---|---|
ENTRY |
指定程序入口点 | ENTRY(Reset_Handler) |
MEMORY |
定义芯片的物理存储区域 | ROM起始0x08000000,RAM起始0x20000000 |
SECTIONS |
描述各段如何分配到MEMORY中 | 核心部分,90%的配置都在这里 |
PROVIDE |
提供符号供C代码引用 | 比如PROVIDE(_estack = ...) |
一个典型的链接脚本结构长这样:
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.text :
{
*(.isr_vector)
*(.text)
*(.text.*)
*(.rodata)
_etext = .;
} > FLASH
.data : AT(_etext)
{
_sdata = .;
*(.data)
_edata = .;
} > RAM
.bss :
{
_sbss = .;
*(.bss)
_ebss = .;
} > RAM
}
关键点:.data段后面的AT(_etext),表示它的加载地址在Flash里,但运行地址在RAM里。这个"双地址"机制,是嵌入式链接脚本最核心的概念。
3.2 ROM/RAM分区——你的芯片地图
每个芯片都有它的"地图"。STM32F103的Flash从0x08000000开始,RAM从0x20000000开始。你想想看,如果链接脚本里把RAM的起始地址写成了0x20001000,那前4KB就永远用不上了——这种坑我踩过。
分区时要注意三点:
- ROM(Flash):存放只读数据——代码、常量、初始化值。掉电不丢失。
- RAM:存放运行时数据——全局变量、堆栈。掉电就丢。
- 加载地址 vs 运行地址:.data段在Flash里存着初值,但运行时必须搬到RAM里。
我个人习惯在MEMORY块里把区域分得细一点:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K /* 有些芯片有CCM RAM */
}
这样后面SECTIONS里就能灵活指定:哪个段放哪个区域。
3.3 三大段:.text / .data / .bss
这三个段,是嵌入式程序的"三驾马车"。搞懂它们,链接脚本就掌握了一半。
.text段——你的代码和常量
.text段包含:中断向量表、函数代码、只读常量(const修饰的变量、字符串字面量)。它必须放在Flash里,因为CPU上电后直接从Flash取指令。
我见过有人把.rodata(只读数据)单独分一个段,其实没必要——放在.text里就行,反正都是只读的。
.data段——有初值的全局变量
这个段最坑。为什么?因为它在Flash里存着初值,但运行时必须在RAM里。链接脚本里必须写清楚:
- 加载地址(LMA):在Flash里,紧跟在.text后面
- 运行地址(VMA):在RAM里,从某个地址开始
启动代码里会有一段搬运程序,把.data从Flash拷到RAM。如果链接脚本写错了,搬运地址对不上——全局变量就全乱套了。
我曾经踩过的坑:有一次我把AT(_etext)写成了AT(LOADADDR(.text)),结果链接器把.data的加载地址算到了.text的起始位置,而不是结束位置。搬运时把代码覆盖了,程序直接跑飞。从那以后,我每次写完链接脚本都会用objdump -h检查各段的地址。
.bss段——零初始化的全局变量
.bss段存放未初始化或初始化为0的全局变量。它在Flash里不占空间,只在RAM里占位置。启动代码需要把它清零。
这里有个细节:_sbss和_ebss这两个符号,是链接器自动生成的,分别表示.bss段的起始和结束地址。启动代码里用它们来算长度:
/* 启动代码中的bss清零 */
ldr r0, =_sbss
ldr r1, =_ebss
mov r2, #0
bss_loop:
cmp r0, r1
itt lo
strlo r2, [r0], #4
blo bss_loop
3.4 段布局的完整流程
咱们用一张图把整个过程串起来:
3.5 实战:一个完整的链接脚本模板
下面是我个人常用的模板,适用于STM32F4系列。你拿去改改芯片的Flash/RAM大小就能用:
/* stm32f407.ld */
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > FLASH
.text :
{
. = ALIGN(4);
*(.text)
*(.text.*)
*(.glue_7)
*(.glue_7t)
*(.eh_frame)
. = ALIGN(4);
_etext = .;
} > FLASH
.rodata :
{
. = ALIGN(4);
*(.rodata)
*(.rodata.*)
. = ALIGN(4);
} > FLASH
_sidata = LOADADDR(.data);
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data.*)
. = ALIGN(4);
_edata = .;
} > RAM AT> FLASH
.bss :
{
. = ALIGN(4);
_sbss = .;
__bss_start__ = _sbss;
*(.bss)
*(.bss.*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
__bss_end__ = _ebss;
} > RAM
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( end = . );
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} > RAM
}
小技巧:_sidata这个符号,我用来记录.data段在Flash里的起始地址。启动代码里搬运时,源地址用_sidata,目标地址用_sdata,长度用_edata - _sdata。这样不管链接脚本怎么改,搬运代码都不用动。
3.6 常见错误与避坑指南
- 段地址重叠:.text和.data的加载地址算重了。用
objdump -h检查各段地址范围。 - 搬运长度算错:
_edata - _sdata是字节数,不是字(word)数。搬运循环里如果用ldr/str一次搬4字节,循环次数要除以4。 - BSS清零漏了:有些启动代码只清了.bss,没清COMMON段。在.bss段里加上
*(COMMON)就能解决。 - 栈顶地址不对:
_estack必须指向RAM的末尾地址,而且要对齐到8字节。我习惯写成_estack = ORIGIN(RAM) + LENGTH(RAM);,简单直接。
我曾经犯过的错:有一次我把.data : { ... } > RAM AT> FLASH写成了.data : { ... } > FLASH AT> RAM。结果链接器把.data段放到了Flash里,但运行地址也指向Flash。全局变量写不进去,程序一跑就死。这个错误在链接阶段不会报错,只有运行了才知道——所以写完链接脚本,一定要用readelf -S或objdump -h确认每个段的LMA和VMA。
链接脚本这东西,说难不难,说简单也不简单。关键是要理解"加载地址"和"运行地址"这两个概念,以及启动代码是怎么配合链接脚本工作的。你把这个搞懂了,后面做Bootloader、做RTOS移植,都会轻松很多。