4. 链接脚本与内存布局:Bootloader 的骨架
链接脚本,说白了就是告诉编译器:你的代码该放哪儿,数据该放哪儿。我刚开始做 Bootloader 时,总觉得这玩意儿可有可无。直到有一次,芯片死活跑不起来,折腾了两天,最后发现是 BSS 段没清零——嗯,从那以后,我再也不敢小看 .lds 文件了。
4.1 链接脚本结构解析
一个典型的 .lds 文件,其实就三大部分:内存布局定义、段定义、符号导出。我们直接看一个实际例子:
/* 内存布局定义 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
/* 段定义 */
SECTIONS
{
.text :
{
_stext = .;
*(.isr_vector) /* 中断向量表 */
*(.text*) /* 代码段 */
*(.rodata*) /* 只读数据 */
_etext = .;
} > FLASH
.data : AT (ADDR(.text) + SIZEOF(.text))
{
_sdata = .;
*(.data*) /* 初始化数据 */
_edata = .;
} > RAM
.bss :
{
_sbss = .;
*(.bss*) /* 未初始化数据 */
*(COMMON)
_ebss = .;
} > RAM
}
这里有几个关键点,我一个个说。
MEMORY 命令:定义物理存储器的起始地址和大小。FLASH 是只读可执行,RAM 是可读写可执行。注意 ORIGIN 和 LENGTH 必须跟芯片手册一致,否则程序跑飞是迟早的事。
4.2 代码段/数据段/BSS 段布局
这三个段,是 Bootloader 的命根子。我画了张图,帮你理清它们的关系:
4.3 栈与堆的设置
栈和堆,是 Bootloader 运行时的两大内存消费者。设置不对,轻则死机,重则启动失败。
我的经验:栈大小一般设为 1KB-4KB 就够用。Bootloader 不像应用层,不需要处理大量嵌套调用。堆的话,如果你不用 malloc,直接设 0 都行。我见过有人给 Bootloader 配了 32KB 的堆,纯属浪费。
栈和堆在链接脚本中通常这样定义:
/* 在 SECTIONS 末尾添加 */
.heap (NOLOAD) :
{
_heap_start = .;
. = . + HEAP_SIZE;
_heap_end = .;
} > RAM
.stack (NOLOAD) :
{
_stack_start = .;
. = . + STACK_SIZE;
_stack_end = .;
} > RAM
/* 在脚本开头定义常量 */
STACK_SIZE = 0x1000; /* 4KB */
HEAP_SIZE = 0x1000; /* 4KB */
这里有个坑,我踩过:栈的生长方向。ARM 是满递减栈,栈指针从高地址往低地址走。所以栈的起始地址要设在 RAM 的最高可用地址附近。我曾经把栈底设在 RAM 中间,结果栈溢出直接覆盖了 .data 段,程序跑着跑着就挂了。
避坑指南:
- 栈顶地址 = RAM 起始 + RAM 大小 - 1(或留 4 字节对齐)
- 堆和栈之间要留足够空间,别让它们撞上
- 启动代码中必须手动初始化栈指针 SP
- BSS 段清零必须在调用任何 C 函数之前完成
4.4 链接脚本中的符号与启动流程
链接脚本里定义的符号,启动代码会直接引用。我列个表,把常用的符号说清楚:
| 符号名 | 含义 | 启动代码用途 |
|---|---|---|
_stext |
代码段起始地址 | 无直接用途,调试用 |
_etext |
代码段结束地址 | 计算 .data 初始值在 FLASH 中的位置 |
_sdata |
数据段起始地址 (RAM) | 拷贝目标地址 |
_edata |
数据段结束地址 (RAM) | 拷贝结束判断 |
_sbss |
BSS 段起始地址 | 清零起始 |
_ebss |
BSS 段结束地址 | 清零结束 |
_stack_end |
栈底地址 | 初始化 SP 寄存器 |
启动代码中,典型的处理流程是这样的:
reset_handler:
/* 1. 初始化栈指针 */
ldr sp, =_stack_end
/* 2. 拷贝 .data 段从 FLASH 到 RAM */
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_etext
bl copy_data
/* 3. 清零 .bss 段 */
ldr r0, =_sbss
ldr r1, =_ebss
bl clear_bss
/* 4. 跳转到 main */
bl main
关键点:第 2 步中,_etext 就是 .data 初始值在 FLASH 中的存放位置。因为 .data 的 LMA(加载地址)紧跟在 .text 后面。这个关系在链接脚本中用 AT (ADDR(.text) + SIZEOF(.text)) 指定了。
4.5 常见链接脚本错误与排查
我这些年调试 Bootloader,链接脚本出问题的情况还真不少。总结几个典型:
- 段重叠:.text 和 .data 的 LMA 算错了,导致数据覆盖代码。用
objdump -h查看各段地址就能发现。 - 符号未定义:启动代码里引用了
_sdata,但链接脚本里写成了_sdata_。编译能过,链接报错。 - 栈溢出:栈设得太小,或者堆栈之间没留缓冲。我习惯在栈底放一个魔数(比如 0xDEADBEEF),启动后检查是否被覆盖。
- 对齐问题:ARM 要求 4 字节对齐,有些外设要求 8 字节。链接脚本里用
. = ALIGN(4);强制对齐。
调试小技巧:编译后用 arm-none-eabi-objdump -t firmware.elf | grep _sdata 查看符号实际地址。再用 arm-none-eabi-readelf -S firmware.elf 看各段信息。两个对照着看,问题一目了然。
嗯,链接脚本这块,说白了就是给 Bootloader 画好内存地图。地图画错了,后面再怎么折腾也是白搭。我建议你拿到一个新芯片,第一件事就是对着手册把 .lds 写对,然后编译、反汇编、确认地址,再往下走。