STM32 裸机启动文件到底在干什么:从 RESET 段、向量表到 Reset_Handler
STM32 裸机启动文件解析
一、 我一开始的疑问
最近在学习 STM32G030C8T6 的极简裸机工程,工程里主要有 Startup.s、main.c、stm32g030xx.h 等文件。看着 Startup.s 这个文件,我脑子里冒出了一堆问题:
.s后缀是不是决定了它就是启动文件?- 文件里定义的
__Vectors、RESET这些名字,是不是 MCU 硬件认识的“魔法”名字? - 向量表第一项
__initial_sp是不是把“栈”放进了 Flash? - 中断函数名
USART1_IRQHandler是怎么和硬件对应起来的?
带着这些疑问,我翻手册、看链接脚本、调试反汇编,终于把这条启动链路给理清了。下面就是我的学习笔记,希望能帮到和我有同样困惑的初学者。
二、 .s 文件不等于启动文件
我一开始以为:文件后缀是 .s(汇编文件),并且名字叫 Startup.s,所以它就是启动文件。
后来发现:.s 后缀只说明它是汇编源文件,需要交给汇编器处理。一个 .s 文件之所以能作为启动文件,不是因为它的名字或后缀,而是因为它提供了 MCU 上电后执行的第一段代码所必需的关键内容:
- 初始栈顶地址 (
__initial_sp) - 中断向量表 (
__Vectors) - 复位处理函数 (
Reset_Handler)
并且,最关键的一步是:链接器要把这些内容最终放到 MCU 复位后会去读取的 Flash 起始地址(通常是 0x08000000)。
所以,更准确的理解是:Startup.s 是一个包含了启动代码的汇编源文件。它的“启动”属性是由其内容和最终的链接位置共同决定的。
三、 AREA RESET, DATA, READONLY 是给链接器看的
启动文件里通常有这样一段代码:
; Startup.s 片段
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors DCD __initial_sp ; 栈顶地址
DCD Reset_Handler ; 复位向量
DCD NMI_Handler
DCD HardFault_Handler
; ... 更多中断向量
我一开始以为:RESET 是 MCU 硬件认识的某个特殊字段,一看到它就知道这是启动代码。
后来发现:AREA RESET, DATA, READONLY 这句汇编指令是给链接器看的,而不是给 MCU 看的。它的作用是定义了一个名为 RESET 的只读数据段。
那么,RESET 这个名字有什么特殊之处呢?这取决于链接器的默认规则。
以 Keil (ARM Compiler) 为例,它的默认链接规则是:优先将名为 RESET 的段放置到 IROM1 区域的起始地址。而在 Keil 的 Target 配置里,IROM1 的起始地址通常被设置为 0x08000000(STM32 内部 Flash 的起始地址)。
这条链路的逻辑是:
Startup.s定义了一个名为RESET的段。- 链接器按照默认规则,把这个
RESET段放到了0x08000000。 - 标签
__Vectors位于RESET段的开头,所以__Vectors的最终地址也就是0x08000000。
结论:RESET 这个名字对 Keil/链接器有意义(决定了存放位置),但对 MCU 硬件没有意义。如果你把 AREA RESET, ... 改成 AREA RESETT, ...,链接器就不再认为它是那个特殊的启动段,可能不会把它放到 0x08000000,程序自然就无法启动了。
四、 __Vectors 不是 MCU 硬件认识的名字
我一开始以为:__Vectors 这个标签名是某种硬件约定,MCU 会去寻找这个名字。
后来发现:__Vectors 只是一个普通的汇编标签,用来标记向量表开始的位置。MCU 硬件根本不认识 __Vectors 这个名字。
MCU 硬件只认死理,遵循 Cortex-M 内核规定的固定启动规则:
上电复位后,从启动地址读取第一个 32 位数,装入 SP 寄存器(作为初始栈顶);再读取第二个 32 位数,装入 PC 寄存器(作为程序入口,即复位向量)。
对于从内部 Flash 启动的 STM32,这个启动地址就是 0x08000000。
所以,只要确保 0x08000000 开始存放的数据,其格式符合 Cortex-M 向量表的规定(第一个字是 SP,第二个字是复位入口地址),MCU 就会把它当作向量表来使用。
因此:__Vectors 能成为向量表,不是因为它的名字特殊,而是因为它(所标记的数据)最终被链接器放到了 0x08000000 这个关键地址,并且里面的数据顺序符合硬件规则。
五、 向量表第 0 项和第 1 项
理解了硬件规则,再看向量表开头就非常清晰了:
__Vectors DCD __initial_sp ; 第 0 项:初始栈顶地址值
DCD Reset_Handler ; 第 1 项:复位处理函数地址
DCD 指令可以理解为“在当前位置分配一个 32 位(4字节)的字,并初始化为后面的值”。
所以,这段汇编的意思是:
- 在
__Vectors标签处(即0x08000000),存放__initial_sp这个符号对应的值(一个32位地址)。 - 紧接着的下一个地址(
0x08000004),存放Reset_Handler这个符号对应的值(另一个32位地址,即函数入口)。
MCU 上电后的动作:
- 从
0x08000000取出第一个 32 位数,装入 SP 寄存器(设置好栈顶)。 - 从
0x08000004取出第二个 32 位数,装入 PC 寄存器(程序计数器)。 - CPU 开始从 PC 指向的地址执行,也就是跳转到
Reset_Handler函数。
这里第一个 32 位数,更准确的叫法是 “初始栈顶地址 (Initial SP)”,而不是“栈尾”。
六、 栈在 RAM,向量表在 Flash
这里有一个容易混淆的点:向量表在 Flash 里,但栈是在 RAM 里的。它们是怎么联系起来的?
启动文件里通常还有另一段定义栈的代码:
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
这段代码定义了一个名为 STACK 的段,属性是 NOINIT, READWRITE,这意味着它最终会被链接到 RAM 区域。SPACE 指令在 STACK 段内分配了 Stack_Size(例如 1KB)的未初始化空间。标签 __initial_sp 紧跟在 Stack_Mem 空间之后,因此它指向了这段 RAM 空间的末尾地址(高地址端)。因为 Cortex-M 的栈是满递减的(向低地址增长),所以初始 SP 必须指向栈空间的最高地址。
关键:__initial_sp 是一个地址值,它代表 RAM 中栈顶的地址(例如 0x20000600)。向量表第一项 DCD __initial_sp,并不是把“栈”这个实体放进 Flash,而是把 “RAM 中栈顶的地址值” 这个数字写进了 Flash 的向量表。
例如,最终在 0x08000000 这个 Flash 地址存放的值可能是 0x20000600。MCU 上电后,只是把这个值 0x20000600 读出来,赋值给 SP 寄存器。真正的栈空间(1KB 的 Stack_Mem)仍然在 RAM 里。
七、 中断函数和硬件是怎么对应起来的
向量表里除了前两项,后面还有很多项,对应着各种中断。
DCD NMI_Handler
DCD HardFault_Handler
; ...
DCD USART1_IRQHandler
; ...
我一开始以为:硬件中断发生时,MCU 会去“查找”名为 USART1_IRQHandler 的函数。
后来发现:硬件根本不认识函数名!中断响应的流程是这样的:
- 固定映射:Cortex-M 内核规定了向量表前 16 项是系统异常(如复位、NMI、硬错误等)。从第 16 项开始,是外部中断(IRQ0, IRQ1, …)。芯片厂商(如 ST)会为每个外设(如 USART1)分配一个固定的 IRQ 编号(例如 USART1 是 IRQ27)。
- 查表跳转:当 USART1 中断发生时,硬件逻辑会根据 IRQ 编号(27)计算出它在向量表中的位置:
向量表基地址 + (16 + 27) * 4。 - 读取地址:硬件去这个地址读取一个 32 位的数值,这个值就是中断服务程序的入口地址。
- 跳转执行:CPU 跳转到这个地址开始执行。
DCD USART1_IRQHandler 这条指令的作用是:让汇编器/链接器把 USART1_IRQHandler 这个符号解析成它对应的函数地址,然后把这个地址值填到向量表对应的位置(比如第 43 项)。
所以:硬件只认向量表中的位置和该位置存放的地址值。函数名 (USART1_IRQHandler) 只是给程序员和链接器看的符号。
八、 默认中断函数为什么经常是死循环
在启动文件里,经常能看到这样的默认中断函数:
HardFault_Handler PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP
USART1_IRQHandler PROC
EXPORT USART1_IRQHandler [WEAK]
B .
ENDP
B . 是一条汇编指令,意思是“跳转到当前位置”,也就是一个死循环。
为什么这么设计?这是一种安全的兜底逻辑。[WEAK] 表示这是一个弱定义。如果用户在工程的其他地方(比如在 main.c 或 stm32g0xx_it.c 里)用 C 语言重新定义了同名的函数:
void USART1_IRQHandler(void) {
// 用户自己的串口中断处理代码
if(USART1->ISR & USART_ISR_RXNE) {
// 读取数据...
}
}
那么,链接器会优先使用用户写的这个强定义,而忽略启动文件里的弱定义。这样,向量表里填的就是用户函数的地址。
如果用户没有写自己的中断处理函数,那么向量表里填的就是启动文件里那个弱符号的地址(B .)。一旦这个中断发生,CPU 就会跳转到那个死循环里。这比让程序跑飞到未知地址要安全,也方便在调试时(程序卡住)发现问题。
九、 最终启动链路总结
让我们把上面所有的点串联起来,看看从点击“下载”到程序运行,到底发生了什么:

- 配置决定位置:我们在 IDE(如 Keil)中配置了 Flash 的起始地址 (
0x08000000)。 - 汇编定义段:
Startup.s通过AREA RESET, ...定义了一个特殊的段。 - 链接器摆放:链接器根据规则,将
RESET段放到配置的 Flash 起始地址。 - 硬件读取:MCU 上电后,机械地从
0x08000000和0x08000004读取数据,设置 SP 和 PC。 - 软件接管:CPU 跳转到
Reset_Handler,由它执行必要的初始化(如初始化 RAM),最后跳转到用户的main函数。
十、 最终修正后的理解
回顾我最开始的误解:
- 误解:MCU 认识
RESET和__Vectors这些名字。 - 正解:
- 链接器认识
RESET这个段名,并据此决定把这段代码/数据放到哪里(0x08000000)。 - MCU 硬件不认识
RESET,也不认识__Vectors。它只遵循 Cortex-M 内核的硬件规则:从固定的启动地址读取向量表。 __Vectors只是程序员和链接器用来标记向量表起始位置的一个标签。
- 链接器认识
再压缩成一句话:
RESET段名决定了链接器把它放到哪里;0x08000000的最终位置决定了 MCU 会不会把它当作向量表;__Vectors只是这个位置上的一个标签名。
希望这篇笔记能帮你拨开 STM32 启动过程的迷雾。理解了这个底层链路,再看启动文件、链接脚本甚至调试时的反汇编,都会清晰很多。
更多推荐

所有评论(0)