3、链接脚本详解:链接脚本的作用、MEMORY命令与REGION定义、SECTIONS命令与段布局
好,咱们进入第三个话题。链接脚本,这玩意儿在Bootloader开发里,可以说是「幕后总指挥」。你写的C代码、汇编代码,最终怎么排布到Flash和RAM里,全听它的。
我刚开始做嵌入式那会儿,总觉得链接脚本是编译器自动搞定的,没必要深究。直到有一次,Bootloader跳转后死活跑不起来,查了三天,最后发现是中断向量表放错了位置。嗯,从那以后,我再也不敢小看这个.lds文件了。
3.1 链接脚本到底在干什么?
说白了,链接脚本就是告诉链接器:你的代码段放哪里,数据段放哪里,栈放哪里。
你想想看,我们写嵌入式程序,不像PC上那样有操作系统帮忙管理内存。芯片就那么点Flash和RAM,地址都是固定的。链接脚本就是一张「地图」,指引链接器把各个目标文件里的段,安排到具体的物理地址上。
我个人习惯,把链接脚本比作「搬家公司的调度单」——每个家具(段)该放哪个房间(内存区域),调度单上写得清清楚楚。
核心作用总结:
- 定义目标芯片的内存布局(Flash、RAM的起始地址和大小)
- 指定各个段的存放位置(.text放Flash,.data放RAM,.bss放RAM)
- 提供符号地址(比如 _estack、_sidata 这些,供启动代码使用)
3.2 MEMORY命令与REGION定义
先看MEMORY命令。这个命令用来描述目标芯片的物理内存资源。
我记得第一次看STM32的链接脚本,里面就写着:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
}
这里定义了两个内存区域:
- FLASH:起始地址0x08000000,大小512KB,属性是rx(可读、可执行)
- RAM:起始地址0x20000000,大小128KB,属性是xrw(可读、可写、可执行)
属性这块要注意:
| 属性 | 含义 |
|---|---|
| r | 可读 |
| w | 可写 |
| x | 可执行 |
| ! | 取反,比如!x表示不可执行 |
我曾经踩过的坑: 有一次做Bootloader,把RAM的起始地址写错了,少写了两个0。结果程序一运行就访问到非法地址,直接HardFault。后来我养成了一个习惯——每次拿到新芯片,第一件事就是对着数据手册,把MEMORY区域的地址和大小核对三遍。
如果你做的是Bootloader + 应用程序的双区架构,通常会这样定义:
MEMORY
{
BOOT_FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 32K
APP_FLASH (rx) : ORIGIN = 0x08008000, LENGTH = 480K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
}
这样就把Flash分成了两个区域:前32KB给Bootloader,后面的480KB给应用程序。为什么要这么分?说白了就是为了方便做OTA升级——升级应用程序时,Bootloader区域不动,即使升级失败,设备也不会变砖。
3.3 SECTIONS命令与段布局
MEMORY定义好了「房间」,SECTIONS就是安排「家具」怎么放了。
一个典型的SECTIONS布局长这样:
SECTIONS
{
/* 中断向量表,必须放在Flash起始地址 */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > BOOT_FLASH
/* 代码段 */
.text :
{
. = ALIGN(4);
*(.text)
*(.text*)
*(.glue_7)
*(.glue_7t)
*(.rodata)
*(.rodata*)
. = ALIGN(4);
_etext = .;
} > BOOT_FLASH
/* 初始化数据段,运行时从Flash复制到RAM */
.data : AT (ADDR(.text) + SIZEOF(.text))
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
. = ALIGN(4);
_edata = .;
} > RAM
/* BSS段,运行时清零 */
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > RAM
}
这里有几个关键点,我一个个说:
3.3.1 段的位置指定
你看每个段后面都有个 > BOOT_FLASH 或 > RAM,这就是告诉链接器:这个段要放在哪个内存区域里。
.isr_vector 和 .text 放在Flash里,因为它们是只读的,掉电不丢失。.data 和 .bss 放在RAM里,因为运行时需要读写。
3.3.2 加载地址与运行地址
注意 .data 段这里有个 AT 语法:
.data : AT (ADDR(.text) + SIZEOF(.text))
这表示:.data 段运行时在RAM里,但加载时(也就是烧录时)放在Flash里,紧跟在 .text 段后面。
为什么会这样?因为芯片上电时RAM里的内容是随机的,所以初始化数据必须预先存在Flash里,启动时由Bootloader代码复制到RAM中。这个复制操作,就是靠 _sdata、_edata 这些符号来定位的。
我的小技巧: 调试时如果发现全局变量初始值不对,十有八九是链接脚本里 .data 段的加载地址和运行地址没配对。我会在启动代码里加个断点,检查 _sdata 和 _sidata(Flash里的源地址)的值对不对。
3.3.3 对齐与符号
代码里频繁出现的 . = ALIGN(4),是让当前地址按4字节对齐。ARM Cortex-M要求中断向量表必须4字节对齐,否则会出问题。
还有那些 _etext、_sdata、_ebss 等符号,它们不是变量,而是地址标签。启动代码里会用到它们:
_sdata:RAM中.data段的起始地址_edata:RAM中.data段的结束地址_sbss:RAM中.bss段的起始地址_ebss:RAM中.bss段的结束地址
启动代码里大概是这样用的:
/* 复制.data段从Flash到RAM */
ldr r0, =_sidata
ldr r1, =_sdata
ldr r2, =_edata
bl copy_data
/* 清零.bss段 */
ldr r0, =_sbss
ldr r1, =_ebss
bl zero_bss
3.4 实战中的链接脚本调整
做Bootloader时,链接脚本经常需要调整。我遇到过几种典型场景:
- 中断向量表重定位:应用程序的向量表不能放在0x08000000了,要偏移到0x08008000。这时需要在应用程序的链接脚本里,把
.isr_vector放到APP_FLASH区域,并且在代码里设置SCB->VTOR = 0x08008000。 - 添加自定义段:比如我想在Flash里存一份固件版本信息,可以定义一个
.version_info段,放在固定地址,方便Bootloader读取。 - 栈顶地址:
_estack通常定义在RAM的末尾,比如_estack = ORIGIN(RAM) + LENGTH(RAM);。这个值会被写入中断向量表的第一个位置,作为复位后的栈指针。
一句话总结: 链接脚本就是嵌入式程序的「地基」。地基没打好,上面盖的楼再漂亮也得塌。花半小时把链接脚本吃透,能省下后面几天的调试时间。
下一节,咱们就基于这个链接脚本,开始写真正的Bootloader启动代码。到时候你会发现,理解了链接脚本,启动代码里的那些地址操作就一目了然了。