3. 链接脚本与内存布局:如何定义 Bootloader 与应用程序的地址空间
说实话,我刚入行那会儿,觉得链接脚本就是个「黑盒子」。编译器帮我搞定了,我干嘛要管它?直到有一次,我写的 Bootloader 跳转到应用程序后,板子直接死机。查了三天,最后发现是链接脚本里把中断向量表的位置写错了。
从那以后,我养成了一个习惯:写固件之前,先把内存布局画清楚。你想想看,芯片就那么点 Flash 和 RAM,Bootloader 占多少、应用程序占多少、中间要不要留升级缓冲区,这些不规划好,后面全是坑。
3.1 为什么内存布局如此重要?
嵌入式系统的内存资源是固定的。Flash 用来存代码和只读数据,RAM 用来存变量和堆栈。Bootloader 和应用程序是两个独立的固件镜像,它们必须「和平共处」在同一块芯片上。
如果地址空间重叠了,会发生什么?
- Bootloader 把自己覆盖了——死机
- 应用程序写入了 Bootloader 的区域——变砖
- 升级过程中断电,两边都坏了——彻底完蛋
核心原则:Bootloader 和应用程序的地址空间必须严格隔离,互不侵犯。这是固件安全的第一道防线。
3.2 典型的内存分区方案
我习惯把 Flash 分成四个区域。这个方案我在好几个量产项目里都用过,稳定可靠。
| 区域 | 起始地址 | 大小 | 内容 |
|---|---|---|---|
| Bootloader 区 | 0x08000000 | 64 KB | Bootloader 代码、中断向量表 |
| 参数存储区 | 0x08010000 | 16 KB | 升级标志、版本号、校验值 |
| 应用程序区 | 0x08014000 | 384 KB | 应用程序代码、中断向量表 |
| 备份/下载区 | 0x08074000 | 112 KB | 升级固件临时存储 |
嗯,这里要注意:中断向量表必须放在每个区域的起始位置。这是芯片硬件决定的——CPU 上电后,会从固定地址读取栈指针和复位向量。Bootloader 的向量表在 0x08000000,应用程序的向量表在 0x08014000。
3.3 链接脚本怎么写?
以 STM32 为例,我们来看看 Bootloader 的链接脚本。我个人习惯用 GCC 工具链,它的链接脚本语法清晰,可读性强。
/* bootloader.ld - Bootloader 链接脚本 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
SECTIONS
{
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > FLASH
.text :
{
. = ALIGN(4);
*(.text*)
*(.rodata*)
. = ALIGN(4);
} > FLASH
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data*)
_edata = .;
} > RAM AT> FLASH
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss*)
_ebss = .;
} > RAM
}
这段脚本做了几件事:
- 定义了 Flash 从 0x08000000 开始,大小 64 KB
- 定义了 RAM 从 0x20000000 开始,大小 20 KB
- 中断向量表放在 Flash 开头
- 代码和只读数据紧随其后
- 可读写数据在 RAM 中,但初始值存储在 Flash 里(AT> FLASH)
小技巧:我习惯在链接脚本里定义一些符号,比如 _sdata、_edata,这样启动代码里可以直接用这些地址来搬运数据。省得手动算偏移量。
3.4 应用程序的链接脚本
应用程序的脚本和 Bootloader 类似,但有两个关键区别:
- Flash 起始地址变了,不再是 0x08000000
- 中断向量表偏移量需要重新设置
/* application.ld - 应用程序链接脚本 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08014000, LENGTH = 384K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
SECTIONS
{
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > FLASH
/* 其余段与 Bootloader 类似,略 */
}
看到区别了吗?Flash 的 ORIGIN 改成了 0x08014000。这意味着应用程序的代码会从 0x08014000 开始存放。
但光改链接脚本还不够。你还需要在应用程序的启动代码里设置中断向量表偏移量寄存器。以 Cortex-M 为例:
/* 应用程序启动代码中设置向量表偏移 */
#define APP_VECTOR_TABLE_ADDR 0x08014000
void SystemInit(void)
{
/* 设置中断向量表偏移 */
SCB->VTOR = APP_VECTOR_TABLE_ADDR;
}
我曾经犯过一个错:只改了链接脚本,忘了改 VTOR。结果应用程序一跑,中断全跑到了 Bootloader 的向量表里。按键按下,本该触发 GPIO 中断,结果执行了定时器中断的服务函数。那场面,简直乱成一锅粥。
警告:链接脚本和 VTOR 必须同步修改。一个改了另一个没改,系统必然崩溃。这是 Bootloader 开发中最常见的错误之一。
3.5 RAM 的分配策略
Flash 的问题解决了,RAM 呢?Bootloader 和应用程序通常共用同一片 RAM。因为 Bootloader 运行完后,它的使命就结束了,RAM 里的数据可以全部释放给应用程序。
但有一个例外:升级过程中需要保存上下文。比如,你在升级前要把一些关键参数存下来,升级完成后还要恢复。这时候,我建议在 RAM 的末尾预留一小块区域,专门用来做数据交换。
/* 在链接脚本中预留共享内存区域 */
.share_buffer (NOLOAD) :
{
. = ALIGN(4);
_sshare = .;
*(.share_buffer*)
_eshare = .;
} > RAM
这个区域不会被 Bootloader 的启动代码清零,也不会被应用程序的启动代码覆盖。两边都能读写,但必须约定好数据格式。
3.6 内存布局的 SVG 示意图
光说可能不够直观。我画了一张内存布局图,你看看就明白了。
3.7 避坑指南
做 Bootloader 内存布局,有几个坑我踩过,你千万别再踩了。
我曾经犯过的错:
- Flash 大小没对齐:有些芯片的 Flash 有擦除扇区大小限制。比如 STM32F103 的扇区是 1 KB 或 2 KB。你分配 Bootloader 为 63 KB,结果最后一个扇区放不下,编译能过,下载就报错。我建议所有分区大小都按扇区对齐。
- 忘了保护 Bootloader 区:很多芯片支持 Flash 写保护。你可以在链接脚本里把 Bootloader 区设为只读,防止应用程序意外写入。我有个项目没开保护,结果一次野指针操作把 Bootloader 区给擦了,板子直接变砖。
- RAM 地址重叠:Bootloader 和应用程序的链接脚本里,RAM 起始地址和大小必须一致。我曾经在两个脚本里用了不同的 RAM 大小,结果 Bootloader 跑完,应用程序启动时栈指针指向了未初始化的区域。
3.8 小结
链接脚本和内存布局,说白了就是给固件画「地皮」。Bootloader 占哪块、应用程序占哪块、中间留多少缓冲区,这些必须在项目一开始就定好。
我个人习惯是先画一张内存布局图,再写链接脚本。图画清楚了,脚本就是填空。你想想看,如果连图都没画,直接上手写脚本,后面改起来得多痛苦?
嗯,这一章的内容就到这儿。记住:内存布局是 Bootloader 的基石,基石不稳,上层建筑全是危房。