第四章 链接脚本与内存布局
做Bootloader开发,绕不开的一个东西就是链接脚本。说实话,我刚入行那会儿,觉得这玩意儿就是个黑盒子——编译器自己会搞定,我干嘛要管它?直到有一次,我写的Bootloader死活跑不起来,调试了整整两天,最后发现是RAM地址配错了。嗯,从那以后,我再也不敢轻视链接脚本了。
4.1 链接脚本到底在干什么?
说白了,链接脚本就是告诉链接器:你的代码该放哪儿,数据该放哪儿,哪些东西要留在Flash,哪些要搬到RAM。你想想看,MCU上电后,Flash里的代码要执行,但变量不能放在Flash里(因为要读写),所以得有个搬运过程。链接脚本就是干这个的。
我个人习惯把链接脚本比作一张「房产证」——它规定了每块内存区域的归属和用途。没有它,编译器就像个无头苍蝇,乱放一气。
核心要点:链接脚本定义了目标文件的段(Section)如何映射到目标硬件的物理内存地址上。
4.2 链接脚本语法速览
GCC用的链接脚本语法,其实没那么复杂。我挑几个最常用的说。
4.2.1 基本结构
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
}
看到没?MEMORY定义了两块区域:Flash和RAM。SECTIONS则告诉链接器,每个段该放哪儿。
小技巧:我在项目中经常用 AT> 这个语法。它表示「这个段运行时在RAM,但初始值存在Flash里」。Bootloader启动时,需要自己把数据从Flash拷贝到RAM——这就是所谓的「重定位」。
4.2.2 常用关键字
- ORIGIN:起始地址。比如Flash从0x08000000开始,这是STM32的典型值。
- LENGTH:大小。别超了,超了链接器会报错。
- AT>:加载地址。运行时地址和加载地址可以不同。
- ALIGN:对齐。我建议你养成对齐的习惯,否则某些外设会罢工。
4.3 代码段、数据段、BSS段
这三个段,是嵌入式开发的老朋友了。但你真的理解它们吗?
4.3.1 .text 段(代码段)
存放可执行指令和只读常量。比如你写的 if、while、函数调用,全在这里。还有 const 修饰的变量,也放这里。
特点:只读,必须放在Flash里。你想想看,代码在RAM里跑当然更快,但RAM贵啊,而且掉电就没了。所以Bootloader的代码段,老老实实放Flash。
注意:我曾经遇到一个坑——把中断向量表也放在了.text段里。结果程序跑飞后,中断向量表被意外修改,整个系统崩溃。后来我强制把向量表单独放在一个段里,并加上只读属性,问题才解决。
4.3.2 .data 段(数据段)
存放已初始化的全局变量和静态变量。比如 int count = 100;,这个 100 的初始值存在Flash里,但 count 这个变量本身必须在RAM里,因为程序运行时会修改它。
所以.data段有两个地址:
- 加载地址(LMA):在Flash里,存放初始值。
- 运行地址(VMA):在RAM里,变量实际的位置。
Bootloader启动时,必须把.data段从Flash拷贝到RAM。这个拷贝动作,通常写在启动文件(startup.s)里。
4.3.3 .bss 段
存放未初始化的全局变量和静态变量。比如 int buffer[1024];,没有赋初值。这些变量在RAM里,但初始值全是0。
为什么叫bss? 我记得是「Block Started by Symbol」的缩写,老古董了。你只要记住:bss段不占用Flash空间,只占用RAM空间。启动时,需要把这段RAM全部清零。
| 段名 | 存储位置 | 内容 | 启动时操作 |
|---|---|---|---|
| .text | Flash | 代码、只读常量 | 无需操作 |
| .data | Flash(初始值)+ RAM(运行时) | 已初始化全局/静态变量 | 从Flash拷贝到RAM |
| .bss | RAM | 未初始化全局/静态变量 | 清零 |
4.4 Flash与RAM分区策略
做Bootloader,分区是个技术活。分不好,轻则空间浪费,重则程序跑飞。
4.4.1 Flash分区
我一般把Flash分成三块:
- Bootloader区:存放Bootloader代码。大小通常16KB~64KB,看功能复杂度。
- 应用区:存放用户应用程序。这是大头,剩下的Flash基本都给它。
- 参数存储区:存放校准参数、配置信息、升级标志等。我习惯放在Flash的最后几页,方便擦写。
/* Flash分区示例 */
#define BOOTLOADER_ADDR 0x08000000
#define BOOTLOADER_SIZE 0x00010000 /* 64KB */
#define APP_ADDR 0x08010000
#define APP_SIZE 0x00070000 /* 448KB */
#define PARAM_ADDR 0x08080000
#define PARAM_SIZE 0x00001000 /* 4KB */
我的经验:Bootloader区最好放在Flash的最开头。因为MCU上电后,默认从0x08000000开始执行。如果你把Bootloader放在别处,还得折腾中断向量表重映射,麻烦得很。
4.4.2 RAM分区
RAM分区相对简单,但有个关键点——中断向量表。
有些MCU允许把中断向量表搬到RAM里,这样可以动态修改中断响应。但Bootloader阶段,我建议别这么干。原因很简单:Bootloader的任务是升级程序,不是跑应用。把向量表留在Flash里,稳定可靠。
RAM分区我通常这样分:
- 栈区:从RAM最高地址往下生长。大小根据函数调用深度定,一般2KB~8KB。
- 堆区:如果用了malloc,需要预留。但Bootloader里我尽量不用动态内存分配。
- 数据区:.data和.bss段,从RAM起始地址开始放。
避坑指南:我曾经在某个项目里,把栈大小设得太小,结果函数递归调用时栈溢出了,程序莫名其妙地复位。查了三天才找到原因。后来我养成了一个习惯——在链接脚本里加上栈溢出检测,比如用 __stack_chk_guard 这种机制。
4.5 实战:一个典型的Bootloader链接脚本
下面是我常用的一个模板,你可以直接拿来改:
/* Bootloader链接脚本示例 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > FLASH
.text :
{
. = ALIGN(4);
*(.text)
*(.text*)
*(.rodata)
*(.rodata*)
. = ALIGN(4);
_etext = .;
} > FLASH
.data : AT(_etext)
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
. = ALIGN(4);
_edata = .;
} > RAM
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss)
*(.bss*)
. = ALIGN(4);
_ebss = .;
} > RAM
}
注意看,我定义了几个符号:_sdata、_edata、_sbss、_ebss。这些符号在启动文件里会被用到,用来实现.data拷贝和.bss清零。
4.6 小结
链接脚本这东西,说难不难,说简单也不简单。你只要记住三点:
- 代码放Flash,变量放RAM。
- 已初始化的变量,初始值存在Flash里,启动时搬到RAM。
- 未初始化的变量,启动时清零。
剩下的,就是根据你的MCU型号和Bootloader大小,调整地址和长度。多试几次,踩几个坑,自然就熟了。
下一章,我们会讲启动流程——也就是Bootloader上电后,到底先干哪件事。到时候你会看到,链接脚本里定义的这些符号,是怎么被用起来的。