20、链接器脚本深入:MEMORY命令,SECTIONS命令,定位符号
说实话,很多C语言开发者写了好几年代码,对链接器脚本还是一头雾水。我刚开始做嵌入式开发时也是这样——编译器能跑就行,管它什么链接脚本?直到有一次,我需要在指定地址放一个bootloader,结果程序死活不按我的想法来。嗯,从那以后,我老老实实把链接器脚本啃了一遍。
今天咱们就聊聊链接器脚本里最核心的三个东西:MEMORY命令、SECTIONS命令和定位符号。说白了,它们就是告诉链接器:你的代码该放哪儿,数据该放哪儿,以及怎么找到它们。
20.1 MEMORY命令:定义你的硬件内存布局
先问个问题:你的芯片里有哪些内存区域?Flash?RAM?地址范围是多少?
MEMORY命令就是干这个的。它让你把目标硬件的物理内存描述清楚。我个人习惯,写任何嵌入式项目的第一步,就是先把MEMORY段定义好。
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
CCRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
}
这里每个内存区域都有三个属性:
- 名字:你自己起的,比如FLASH、RAM
- 属性:r=读,w=写,x=执行
- 起始地址和长度:硬件手册上查来的
你可能会问:为什么要有CCRAM这种区域?我在项目中遇到过,有些MCU有紧耦合内存(TCM),访问速度比普通RAM快很多。把中断向量表或者实时性要求高的数据放进去,性能提升很明显。
20.2 SECTIONS命令:把内容装进容器
MEMORY定义了「容器」,SECTIONS就是决定「什么东西装进哪个容器」。这是链接器脚本里最复杂、也最灵活的部分。
SECTIONS
{
.text :
{
*(.text)
*(.text.*)
*(.rodata)
*(.rodata.*)
} > FLASH
.data :
{
_sdata = .;
*(.data)
*(.data.*)
_edata = .;
} > RAM AT> FLASH
.bss :
{
_sbss = .;
*(.bss)
*(.bss.*)
*(COMMON)
_ebss = .;
} > RAM
}
这段脚本干了三件事:
- .text段:放代码和只读数据,放到FLASH里
- .data段:放初始化的全局变量,运行时在RAM里,但初始值存在FLASH里(AT> FLASH就是这个意思)
- .bss段:放未初始化的全局变量,只占RAM空间
.data段的AT> FLASH是什么意思?它表示:这个段的加载地址在FLASH,但运行地址在RAM。启动代码需要把数据从FLASH拷贝到RAM。这就是嵌入式系统里经典的「数据段重定位」。
我刚开始学的时候,一直搞不懂为什么.data要占两份空间。直到有一次做OTA升级,发现固件大小超了——原来.data段在FLASH里也占了一份,算总大小时得算上。嗯,这个坑我踩过。
20.3 定位符号:链接器给C代码的暗号
定位符号,就是那些以.开头的特殊变量。比如上面的_sdata、_edata、_sbss、_ebss。它们不是真正的变量,而是链接器计算出来的地址值。
在C代码里怎么用?看这个启动代码片段:
extern uint32_t _sdata, _edata, _sbss, _ebss;
extern uint32_t _sidata; // .data在FLASH里的起始地址
void reset_handler(void)
{
// 拷贝.data段
uint32_t *src = &_sidata;
uint32_t *dst = &_sdata;
while (dst < &_edata) {
*dst++ = *src++;
}
// 清零.bss段
dst = &_sbss;
while (dst < &_ebss) {
*dst++ = 0;
}
// 跳转到main
main();
}
你看,链接器脚本里定义的_sdata,在C代码里用extern声明一下就能用了。链接器在链接阶段会把它们的值填好。这就是链接器给C代码传递信息的「暗号」。
extern uint32_t,不能用extern uint8_t或extern uint16_t。因为链接器处理的是地址,地址永远是32位的(在32位系统上)。我曾经用extern uint8_t _sdata,结果取地址时只取了低8位,程序直接跑飞。
20.4 一个完整的例子:从脚本到运行
咱们把上面的东西串起来,画个图看看整个流程:
这个图展示了整个流程:链接器脚本定义内存布局 → 链接器根据脚本分配地址 → 运行时启动代码完成数据拷贝和BSS清零。每一步都离不开MEMORY、SECTIONS和定位符号的配合。
20.5 实战中的几个坑
最后分享几个我踩过的坑,希望能帮你省点调试时间:
很多人写
uint32_t data_start = _sdata;,以为拿到了.data段第一个字节的内容。错了!_sdata本身就是地址值,你应该用uint32_t *p = &_sdata;来取地址。
> RAM指定运行地址,AT> FLASH指定加载地址。如果你只写> RAM,链接器会认为数据本来就在RAM里——那上电时数据从哪来?全乱套了。
SECTIONS里段的顺序,就是它们在内存里的顺序。如果你把.bss写在.data前面,那BSS段就会占据RAM的低地址。有些启动代码写死了地址偏移,一改顺序就崩。
嗯,链接器脚本这东西,说难不难,说简单也不简单。关键是要理解它背后的逻辑:你告诉链接器硬件长什么样,它帮你把程序安排得明明白白。下次遇到链接错误,别急着怀疑编译器,先看看你的链接脚本对不对。
*(.text.*),结果某些函数被优化掉,程序莫名其妙少功能。
好了,关于MEMORY、SECTIONS和定位符号,今天就聊到这儿。这些东西看着枯燥,但用好了,你能精确控制程序的每一字节放在哪。在嵌入式、操作系统开发、甚至一些性能优化场景下,这都是必备技能。