从寄存器到链接脚本:手把手拆解 STM32 启动文件与中断向量表重定向
对于有一定基础的嵌入式开发者而言,启动文件(startup_*.s)往往是一个“黑盒”——知道它重要,却很少深究。本文将带你从寄存器层面理解启动流程,并实战中断向量表重定向。
一、启动文件到底做了什么?
以 STM32F103 的 startup_stm32f103xb.s 为例,复位后硬件自动完成:
- 从地址
0x00000000(映射到 Flash 或 System memory)取出初始 MSP(主栈指针)。 - 从
0x00000004取出复位向量(Reset_Handler 地址)。 - 跳转到 Reset_Handler 执行。
启动文件的核心任务:
- 定义中断向量表(位于 Flash 起始处)。
- 初始化栈指针(通过
__initial_sp符号)。 - 调用
SystemInit配置时钟。 - 调用
__main完成数据段(.data)从 Flash 到 RAM 的拷贝、BSS 段清零,最后跳转到main()。
二、中断向量表结构
向量表是一个 4 字节对齐的数组,前两项固定为 MSP 和 Reset_Handler,后续为各外设中断服务函数地址。
// 典型向量表片段(C 语言表示,实际在汇编中定义)
extern uint32_t _estack;
void Reset_Handler(void);
void NMI_Handler(void) __attribute__((weak, alias("Default_Handler")));
__attribute__((section(".isr_vector")))
const uint32_t vector_table[] = {
(uint32_t)&_estack, // 0x00: 初始 MSP
(uint32_t)Reset_Handler, // 0x04: 复位向量
(uint32_t)NMI_Handler, // 0x08: NMI
// ... 其他中断
};
链接脚本通过 .isr_vector 段将其放置在 Flash 起始地址。
三、为什么要重定向向量表?
常见场景:
- Bootloader + App 架构:App 运行在偏移地址(如 0x08008000),需将向量表指向自身。
- 动态修改中断服务函数:在 RAM 中重建向量表,实现运行时替换。
- 多核/安全隔离:不同核心使用独立向量表。
STM32 通过 SCB->VTOR(向量表偏移寄存器,地址 0xE000ED08)控制向量表基址。复位后 VTOR 默认为 0,但若从 Flash 启动,实际基址为 0x08000000(取决于 BOOT 引脚映射)。
四、重定向实战:将向量表搬到 RAM
4.1 修改链接脚本
在 .ld 文件中定义 RAM 中的向量表段,并保留 Flash 中的原始表:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}
SECTIONS
{
.isr_vector : {
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} >FLASH
.ram_vector : {
. = ALIGN(4);
_ram_vector_start = .;
KEEP(*(.ram_vector))
. = ALIGN(4);
_ram_vector_end = .;
} >RAM AT> FLASH
/* 其他段省略 */
}
4.2 定义 RAM 向量表并拷贝
在 C 代码中定义 RAM 向量表,并在 main() 前完成拷贝和 VTOR 设置:
// 定义 RAM 向量表(大小根据芯片中断数调整)
__attribute__((section(".ram_vector")))
const uint32_t ram_vector_table[64] = {
(uint32_t)&_estack,
(uint32_t)Reset_Handler,
// 其余中断向量可后续动态填充
};
void relocate_vector_table(void)
{
extern uint32_t _ram_vector_start, _ram_vector_end;
uint32_t *src = (uint32_t *)&_ram_vector_start;
uint32_t *dst = (uint32_t *)&_ram_vector_end;
// 注意:此处仅为示意,实际需从 Flash 拷贝到 RAM
// 更简单的方式:直接使用链接脚本的 AT> 加载地址
// 但运行时需手动拷贝
uint32_t size = (uint32_t)&_ram_vector_end - (uint32_t)&_ram_vector_start;
memcpy((void *)0x20000000, (void *)&_ram_vector_start, size);
// 设置 VTOR 指向 RAM 起始地址
SCB->VTOR = 0x20000000;
__DSB();
__ISB();
}
注意:上述代码中 _ram_vector_start 是 RAM 中的地址,但链接脚本 AT> FLASH 表示其加载地址在 Flash。实际拷贝时需使用加载地址(LOADADDR)。更稳妥的做法:
extern uint32_t _sram_vector, _eram_vector, _sload_vector;
memcpy(&_sram_vector, &_sload_vector, (size_t)(&_eram_vector - &_sram_vector));
4.3 在启动文件中调用
修改 Reset_Handler,在 SystemInit 之后、__main 之前调用 relocate_vector_table()。
五、关键注意事项
- 对齐要求:VTOR 必须按向量表大小对齐(通常 128 字节或 512 字节),否则硬件可能取指错误。
- 中断使能时机:重定向完成前不要使能任何中断,否则可能跳转到旧向量表。
-
内存屏障:修改 VTOR 后需执行
__DSB()和__ISB()确保生效。 -
Bootloader 场景:App 的链接脚本需设置
FLASH ORIGIN = 0x08008000,并在 App 的main()开头设置SCB->VTOR = 0x08008000。 - 调试影响:重定向后,调试器可能无法正确解析中断,需手动加载符号。
六、总结
启动文件与向量表是嵌入式系统的“第一行代码”。理解 MSP 初始化、向量表布局以及 VTOR 的作用,不仅能帮你解决 Bootloader 跳转异常、中断不响应等疑难杂症,更是迈向底层开发的必经之路。动手修改一次链接脚本,你将对“程序如何跑起来”有全新的认识。