4. 链接脚本深度解析:分散加载文件(.sct)结构、ROM/RAM分区定义、中断向量表重定位
做 Bootloader 开发,绕不开链接脚本。说白了,它就是告诉编译器:你的代码该放哪儿,数据该放哪儿,中断向量表又该在哪儿安家。很多新手觉得这玩意儿枯燥,直接拿厂家例程改改就用了。嗯,我早年也这么干过,结果有一次 Bootloader 跳转后死活跑不起来,查了三天,最后发现是向量表没对齐——从那以后,我再也不敢轻视 .sct 文件了。
4.1 分散加载文件的结构
ARM 编译器用的 .sct 文件,其实就是一个分区地图。它把芯片的存储空间切成若干块,然后告诉链接器:哪个段(Section)该放进哪个块。
一个典型的 .sct 文件长这样:
LR_IROM1 0x08000000 0x00010000 { ; 加载区,起始0x08000000,大小64KB
ER_IROM1 0x08000000 0x00010000 { ; 执行区,与加载区重叠
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00008000 { ; RAM区,起始0x20000000,大小32KB
.ANY (+RW +ZI)
}
}
结构其实不复杂,就三层:
- 加载区(Load Region):程序烧录时在 Flash 里的位置。名字可以随便起,比如 LR_IROM1。
- 执行区(Execution Region):程序运行时在内存里的位置。可以跟加载区重叠(比如代码直接在 Flash 里跑),也可以不同(比如 RW 数据要搬到 RAM)。
- 输入段(Input Section):具体哪些 .o 文件、哪些段名放进来。比如
*.o (RESET, +First)就是把所有目标文件的 RESET 段放在最前面。
关键点:加载区的大小必须跟芯片 Flash 容量匹配。我见过有人把 LR_IROM1 的大小写成了 0x20000(128KB),结果芯片只有 64KB Flash,链接器也不报错,烧录时直接写飞了。
4.2 ROM/RAM 分区定义
Bootloader 和应用程序通常共用一颗芯片。怎么分家?靠的就是 ROM/RAM 分区。
我个人习惯把 Flash 分成三块:
| 分区 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader 区 | 0x08000000 | 32KB | 存放 Bootloader 代码、固定参数 |
| 参数区 | 0x08008000 | 4KB | 升级标志、版本号、校验值 |
| 应用程序区 | 0x08009000 | 剩余空间 | 用户应用程序 |
对应的 .sct 文件就要写两个加载区:
; Bootloader 区
LR_BOOT 0x08000000 0x00008000 {
ER_BOOT 0x08000000 0x00008000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_BOOT_RAM 0x20000000 0x00004000 {
.ANY (+RW +ZI)
}
}
; 参数区
LR_PARA 0x08008000 0x00001000 {
ER_PARA 0x08008000 0x00001000 {
*(.param_section)
}
}
; 应用程序区(由应用程序自己的 .sct 定义)
经验之谈:参数区我建议单独划出来,不要跟代码混在一起。这样升级应用程序时,Bootloader 只需要擦除应用程序区,参数区里的版本号和校验值还能保留。我曾经踩过坑——参数跟代码放一起,一次擦除全没了,升级到一半断电,板子直接变砖。
RAM 分区也要注意。Bootloader 和应用程序如果共用 RAM,跳转前必须把 Bootloader 用过的全局变量、堆栈都清理干净。否则应用程序一启动,发现 RAM 里全是垃圾数据,跑着跑着就 HardFault 了。
我常用的做法是:Bootloader 只用低 16KB RAM,应用程序从 0x20004000 开始用。这样跳转前只需要把 0x20000000~0x20003FFF 清零就行,简单粗暴。
4.3 中断向量表重定位
这是 Bootloader 跳转的灵魂操作。芯片上电后,CPU 从 0x00000000 取栈顶指针,从 0x00000004 取复位向量。但 CMS32 这类 Cortex-M 内核的芯片,实际上会把 0x08000000 映射到 0x00000000(通过硬件配置)。
问题来了:Bootloader 在 0x08000000,应用程序在 0x08009000。应用程序的中断向量表在 0x08009000,但 CPU 默认还是去 0x08000000 找向量表。怎么办?
答案就是修改 VTOR(向量表偏移寄存器)。
跳转前,Bootloader 必须做这件事:
// 假设应用程序起始地址为 APP_ADDR (0x08009000)
uint32_t app_stack = *(volatile uint32_t*)APP_ADDR;
uint32_t app_reset = *(volatile uint32_t*)(APP_ADDR + 4);
// 1. 检查栈顶指针是否合法(通常在 RAM 范围内)
if (app_stack < 0x20000000 || app_stack >= 0x20010000) {
// 非法,不跳转
return;
}
// 2. 关闭全局中断
__disable_irq();
// 3. 重定位向量表
SCB->VTOR = APP_ADDR;
// 4. 设置主栈指针
__set_MSP(app_stack);
// 5. 跳转到应用程序复位向量
((void (*)(void))app_reset)();
注意:跳转前一定要关闭所有外设中断!我遇到过最诡异的问题:跳转后应用程序刚初始化串口,就触发了 Bootloader 时期未清除的 DMA 中断,直接卡死在中断服务函数里。所以我的习惯是跳转前把所有外设时钟关了,中断标志清了,再关全局中断。
另外,应用程序的 .sct 文件也要配合修改。它的加载区起始地址必须跟 Bootloader 约好的 APP_ADDR 一致:
LR_APP 0x08009000 0x00070000 {
ER_APP 0x08009000 0x00070000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_APP_RAM 0x20004000 0x0000C000 {
.ANY (+RW +ZI)
}
}
你看,应用程序的 RESET 段在 0x08009000,所以它的向量表也在那里。Bootloader 把 VTOR 改成 0x08009000 后,CPU 就能找到应用程序的中断入口了。
4.4 整体流程梳理
为了让你更直观地理解,我画了一张流程图:
这张图把 Flash 分区、跳转步骤和检查点串在了一起。你写代码时,对着这张图一步步来,基本不会漏。
4.5 避坑指南
最后,我把自己这些年踩过的坑列出来,你写 .sct 和跳转代码时对照着检查:
- 向量表对齐:Cortex-M 要求 VTOR 的值必须是 256 的倍数(低 9 位为 0)。应用程序的起始地址如果没对齐,跳转后所有中断都进不去。
- 栈顶指针检查:跳转前一定要读应用程序的栈顶指针,确认它在 RAM 范围内。如果 Flash 是空的或者数据被破坏了,读出来可能是 0xFFFFFFFF,直接设置 MSP 会立刻 HardFault。
- 外设状态清理:我曾经在跳转前忘了关定时器,结果应用程序初始化时定时器溢出触发了中断,而应用程序的中断向量表还没配好,直接跑飞。
- 链接脚本命名冲突:Bootloader 和应用程序的 .sct 文件里,加载区名字不要一样。比如 Bootloader 用 LR_BOOT,应用程序用 LR_APP,否则链接器可能搞混。
我的习惯:每次改完 .sct 文件,我都会用 fromelf --text -a -c output.axf 反汇编看看,确认 RESET 段确实在预期的地址上。花 5 分钟看一眼,能省下半天调试时间。