链接脚本与内存布局:给芯片画一张精确的地图
做 Bootloader 开发,第一道坎就是搞懂链接脚本。说白了,链接脚本就是告诉编译器:「你的代码该放哪儿,数据该放哪儿,入口在哪。」没有它,你编译出来的二进制文件就是一盘散沙。
我记得刚入行那会儿,接手一个老项目,链接脚本写得一塌糊涂。Flash 和 RAM 区域重叠了都没人发现,结果芯片一上电就跑飞。从那以后,我养成了一个习惯——每次新项目,第一件事就是亲手写链接脚本。
链接脚本到底在干什么?
你想想看,我们写 C 代码的时候,函数、全局变量、静态变量,这些东西最终都要落到具体的物理地址上。链接脚本就是那个「分配师」。它负责:
- 定义存储区域:告诉链接器 Flash 从哪开始、RAM 从哪开始
- 指定段布局:代码段(.text)、数据段(.data)、BSS 段(.bss)各放哪
- 设置入口点:芯片复位后第一条指令从哪取
- 定位中断向量表:中断来了,CPU 该去哪查表
核心认知:链接脚本不是「可有可无」的配置,它是固件能否正确运行的基石。一个错误的链接脚本,足以让你的 Bootloader 在产线上全军覆没。
Flash 和 RAM 区域怎么定义?
以 STM32F103 为例,它的 Flash 从 0x08000000 开始,大小 512KB;RAM 从 0x20000000 开始,大小 64KB。在链接脚本里,我们这样定义:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}
这里有几个关键点:
- ORIGIN:起始地址,必须和芯片手册一致
- LENGTH:可用大小,别写多了,芯片没那么多空间
- (rx)/(rwx):权限属性,Flash 只读可执行,RAM 可读写可执行
我的习惯:我会在 MEMORY 块里多定义一个「BOOTLOADER」区域,专门给 Bootloader 用。这样应用代码和 Bootloader 的地址空间彻底隔离,OTA 升级时不容易出问题。
入口地址与中断向量表
芯片上电后,CPU 会去一个固定的地址取第一条指令。对于 Cortex-M 系列,这个地址是 0x00000000,但实际映射到 Flash 的起始位置。所以,入口地址通常就是 Flash 的 ORIGIN。
中断向量表必须放在入口地址处。为什么?因为 CPU 复位后,会从向量表的第一个字(栈顶指针)和第二个字(复位向量)开始执行。我见过有人把向量表放偏了,结果中断一进来就死机。
SECTIONS
{
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* 中断向量表 */
. = ALIGN(4);
} > FLASH
.text :
{
. = ALIGN(4);
*(.text) /* 代码段 */
*(.text*)
. = ALIGN(4);
} > FLASH
.data :
{
. = ALIGN(4);
_sdata = .; /* 数据段起始 */
*(.data)
*(.data*)
. = ALIGN(4);
_edata = .; /* 数据段结束 */
} > RAM AT> FLASH
.bss :
{
. = ALIGN(4);
_sbss = .; /* BSS 段起始 */
*(.bss)
*(.bss*)
. = ALIGN(4);
_ebss = .; /* BSS 段结束 */
} > RAM
}
注意看 .data 段后面的 > RAM AT> FLASH。这表示:运行时数据在 RAM 里,但初始值存储在 Flash 中。启动代码需要把这段数据从 Flash 拷贝到 RAM。这个细节,很多人会忽略。
我曾经踩过的坑:有一次做 OTA 升级,新固件下载完后,校验通过,但重启后就是跑不起来。查了两天,最后发现是链接脚本里 .data 段的 LMA(加载地址)和 VMA(运行地址)没对齐。拷贝的时候少拷贝了几个字节,全局变量全乱了。从那以后,我每次都会检查 _sdata 和 _edata 的差值是否和预期一致。
Bootloader 特有的内存布局
Bootloader 和应用代码共享同一颗芯片,但它们的地址空间必须严格隔离。我常用的布局是这样的:
| 区域 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader Flash | 0x08000000 | 32KB | Bootloader 代码 + 向量表 |
| 参数存储区 | 0x08008000 | 4KB | 升级标志、版本号等 |
| 应用代码区 | 0x08009000 | 448KB | 应用固件 |
| 备份区 | 0x08080000 | 64KB | OTA 下载暂存 |
| RAM | 0x20000000 | 64KB | 运行时数据 |
这样设计的好处是:Bootloader 永远不会被应用代码覆盖。即使 OTA 升级失败,Bootloader 依然健在,可以重新下载固件。
SVG:链接脚本与内存布局全景图
实际项目中的避坑指南
链接脚本看起来简单,但实际项目中坑不少。我总结了几条:
- 向量表对齐:Cortex-M 要求向量表 256 字节对齐。如果没对齐,中断可能跳到错误的地方。
- LMA 和 VMA 要分清:.data 段的加载地址(LMA)在 Flash,运行地址(VMA)在 RAM。启动代码必须做拷贝。
- BSS 段清零:未初始化的全局变量默认是 0,但硬件不会自动清零。启动代码要手动清 .bss 段。
- 栈大小别设太小:我见过有人栈设了 256 字节,函数调用深一点就栈溢出。Bootloader 里至少留 1KB。
- OTA 升级时的地址偏移:应用代码的链接脚本里,Flash 起始地址要偏移 Bootloader 占用的空间。否则两个固件会打架。
我的调试技巧:编译完成后,用 objdump -h 查看各段的 VMA 和 LMA,用 objdump -t 查看符号地址。如果发现某个全局变量的地址不在 RAM 范围内,那肯定是链接脚本写错了。
总结一下
链接脚本是 Bootloader 开发的「宪法」。它定义了代码和数据在芯片里的「居住权」。搞懂了它,你就能精确控制每一字节的去向。搞不懂它,你的固件就像没有地图的探险队——迟早会迷路。
嗯,这一章的内容就到这里。记住:写链接脚本的时候,多花十分钟检查,能省下后面十小时的调试时间。