链接脚本:ld链接脚本语法,内存区域定义,节区放置规则
说到链接脚本,我得先坦白一件事。我刚开始学嵌入式那会儿,觉得这东西就是个黑盒子。编译器把 .o 文件扔进去,出来一个 .bin,中间发生了什么?说实话,我用了两年 GNU 工具链,都没认真看过默认的 ld 脚本长什么样。
直到有一次,我自己做一块 STM32 的板子,RAM 只有 64K,Flash 256K。默认链接脚本把 .rodata 扔到了 RAM 里,程序一跑就崩。我查了三天,最后发现是链接脚本没指定只读数据放到 Flash 段。嗯,从那以后,我再也不敢跳过 ld 脚本了。
说白了,链接脚本就是告诉链接器:你的代码该放哪儿,数据该放哪儿,堆栈怎么安排。今天咱们就把这东西彻底讲透。
1. 链接脚本的基本结构
一个典型的 ld 脚本,长这样:
OUTPUT_FORMAT("elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.text : {
*(.text)
*(.rodata)
} > FLASH
.data : {
*(.data)
} > RAM AT> FLASH
.bss : {
*(.bss)
} > RAM
}
你看,核心就三块:OUTPUT 设置、MEMORY 定义、SECTIONS 规则。咱们一个一个拆开讲。
2. MEMORY:内存区域定义
MEMORY 块用来描述目标芯片的物理内存布局。每个区域有四个属性:名字、权限、起始地址、长度。
| 属性 | 含义 | 示例 |
|---|---|---|
| 名字 | 自定义标识符 | FLASH, RAM, SRAM1 |
| 权限 | r=读, w=写, x=执行 | (rx), (rwx), (rw) |
| ORIGIN | 起始地址 | 0x08000000 |
| LENGTH | 区域大小 | 512K, 128M, 0x10000 |
我个人习惯把 MEMORY 定义放在脚本最前面。为什么?因为后面的 SECTIONS 要引用这些区域,先定义清楚,后面写起来才顺手。
3. SECTIONS:节区放置规则
SECTIONS 是链接脚本的核心。它定义了每个节区(section)该放到哪个内存区域,以及节区之间的顺序。
基本语法是这样的:
SECTIONS
{
.节区名 : {
输入文件列表
} > 内存区域
}
举个例子:
.text : {
*(.text) /* 所有 .o 文件的 .text 段 */
*(.rodata) /* 所有 .o 文件的 .rodata 段 */
_etext = .; /* 记录 .text 结束地址 */
} > FLASH
这里有个小技巧:_etext = .; 这行代码定义了一个符号,它的值等于当前地址计数器的值。你可以在 C 代码里用 extern uint32_t _etext; 来获取这个地址。我在做启动代码时经常用这招,用来计算初始化数据的长度。
4. 地址计数器:点号(.)的秘密
点号 . 是链接脚本里的地址计数器。它表示当前正在处理的虚拟地址。每次往节区里放数据,点号就会自动增加。
. = ALIGN(4); /* 对齐到 4 字节 */
.data : {
*(.data)
} > RAM
为什么要对齐?因为 ARM 处理器访问未对齐的数据会触发异常。我踩过这个坑:结构体里有个 uint32_t 成员,地址没对齐,程序跑着跑着就进 HardFault 了。查了半天,最后发现是链接脚本里忘了加 ALIGN。
. = ALIGN(4); 或者 . = ALIGN(8);。这不会浪费多少空间,但能避免很多奇怪的崩溃问题。
5. AT 关键字:加载地址 vs 运行地址
这是链接脚本里最容易让人迷糊的地方。一个节区有两个地址:
- 加载地址(LMA):程序烧录时存放的位置,通常是 Flash
- 运行地址(VMA):程序运行时实际的位置,通常是 RAM
看这个例子:
.data : {
*(.data)
} > RAM AT> FLASH
意思是:.data 段运行时在 RAM(VMA = RAM),但它的初始值存储在 Flash 里(LMA = FLASH)。启动代码需要把这段数据从 Flash 拷贝到 RAM。
我记得第一次写裸机程序时,没加 AT> FLASH,结果 .data 段直接烧到了 RAM 里。掉电重启后,所有全局变量都变成了随机值。嗯,那次教训挺深刻的。
6. 实战:一个完整的链接脚本
下面是我个人常用的一个模板,适用于 Cortex-M3/M4 芯片:
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
. = ALIGN(4);
.text : {
KEEP(*(.isr_vector)) /* 中断向量表,必须保留 */
*(.text)
*(.text*)
*(.rodata)
*(.rodata*)
_etext = .;
} > FLASH
. = ALIGN(4);
.data : {
_sdata = .;
*(.data)
*(.data*)
_edata = .;
} > RAM AT> FLASH
. = ALIGN(4);
.bss : {
_sbss = .;
*(.bss)
*(.bss*)
*(COMMON)
_ebss = .;
} > RAM
. = ALIGN(8);
_estack = ORIGIN(RAM) + LENGTH(RAM);
}
这个脚本里我做了几件事:
- 用
KEEP保护中断向量表,防止被优化掉
.data 段定义了 - .bss 段定义了
_sbss和_ebss,用于清零 - 栈顶
_estack直接算出来,放在 RAM 末尾
_sdata 和 _edata,方便启动代码做拷贝
7. 知识体系总览
下面这张图总结了链接脚本的核心逻辑:
8. 避坑指南
最后,分享几个我踩过的坑:
- 忘记 KEEP 中断向量表:链接器觉得 .isr_vector 没被引用,直接优化掉了。结果芯片上电后找不到复位入口,直接死机。
- AT 和 > 写反:
> FLASH AT> RAM和> RAM AT> FLASH完全是两回事。写反了数据就放错地方了。 - 栈顶地址算错:我见过有人把栈顶写在 RAM 起始地址,结果一压栈就把代码数据覆盖了。
- 节区顺序搞乱:.text 必须在最前面,因为 Flash 的起始地址就是复位向量表。如果把 .data 放前面,芯片上电后找不到入口。
链接脚本这东西,说白了就是给芯片画地图。地图画对了,程序跑起来就顺;画错了,各种奇怪问题都会冒出来。我建议你每次新建项目时,都从芯片厂商提供的默认脚本开始改,不要自己从头写。等用熟了,再慢慢调整优化。
核心要点回顾:
- MEMORY 定义物理内存,SECTIONS 定义逻辑布局
- 地址计数器 . 代表当前位置,ALIGN 保证对齐
- AT 关键字区分加载地址和运行地址
- KEEP 保护关键节区不被优化
- 启动代码负责搬运 .data 和清零 .bss
公众号:蓝海资料掘金营,微信deep3321