4. 汇编启动代码:复位向量处理函数、堆栈指针初始化、跳转到C语言main函数
好,咱们进入正题。上一章我们聊了链接脚本,把内存布局安排得明明白白。但芯片上电后,第一条指令从哪里开始执行?谁把C语言的世界搭建起来的?答案就在本章——汇编启动代码。
说白了,启动代码就是一段“暖场”程序。它负责把硬件从复位状态拉起来,把堆栈指针指到正确位置,然后优雅地把控制权交给 main()。没有它,你的C代码连个局部变量都定义不了。
- 定义复位向量,告诉CPU从哪里启动
- 初始化堆栈指针(SP),让C语言有栈可用
- 跳转到
main(),进入C世界
4.1 复位向量——芯片的“出生证明”
ARM Cortex-M系列芯片上电后,硬件会自动从 0x00000000 地址读取两个关键值:
| 偏移地址 | 内容 | 作用 |
|---|---|---|
| 0x00000000 | 初始堆栈指针(MSP) | 给主堆栈一个起点 |
| 0x00000004 | 复位向量(Reset_Handler) | 第一条指令的入口地址 |
嗯,这里要注意:向量表第一个字是SP,第二个字才是复位向量。我刚开始做Bootloader时,曾把这两个值写反了,结果芯片死活跑不起来。后来用调试器一看,PC指针飞到了火星上……
在汇编里,我们通常这样定义向量表:
; 文件: startup.s
.section .isr_vector, "a"
.global _estack
.global Reset_Handler
.word _estack ; 0x00000000: 栈顶地址
.word Reset_Handler ; 0x00000004: 复位入口
.word NMI_Handler ; 非屏蔽中断
.word HardFault_Handler ; 硬错误
; ... 其他中断向量省略
_estack 是链接脚本里定义的栈顶地址。你想想看,如果这个值不对,函数调用时压栈就会写到非法内存,后果很严重。
4.2 堆栈指针初始化——给C语言搭个台子
复位后,硬件已经自动把 _estack 加载到了主堆栈指针(MSP)。但有些场景下,我们需要手动重新设置SP。比如:
- 多堆栈模式:Bootloader和APP使用不同的栈区域
- 安全隔离:防止栈溢出污染关键数据
- 调试需求:我想让栈底留个哨兵值,检测溢出
手动设置SP的汇编代码很简单:
Reset_Handler:
; 设置主堆栈指针
LDR r0, =_estack
MSR msp, r0
; 可选:设置进程堆栈指针(PSP)
; LDR r0, =_psp_stack
; MSR psp, r0
我个人习惯在复位后立即设置一次SP,即使硬件已经做了。为什么?因为有些芯片的复位序列并不保证SP正确加载,尤其是从低功耗模式唤醒时。我在一个IoT项目里遇到过,芯片深度睡眠后复位,SP指向了随机值,一调用函数就HardFault。从那以后,我每条启动代码都加这句,求个心安。
4.3 跳转到main()——最后一公里
SP就位后,接下来要做几件“家务活”,然后才能跳转到C语言:
- 清零BSS段:未初始化的全局变量必须归零
- 拷贝数据段:从Flash把初始值搬到RAM
- 调用构造函数:C++全局对象的构造(如果有)
- 跳转main():进入C世界
完整的启动流程如下:
Reset_Handler:
; 1. 设置堆栈指针
LDR r0, =_estack
MSR msp, r0
; 2. 清零BSS段
LDR r0, =__bss_start
LDR r1, =__bss_end
MOVS r2, #0
bss_loop:
CMP r0, r1
ITT LT
STRLT r2, [r0], #4
BLT bss_loop
; 3. 拷贝数据段
LDR r0, =__data_start
LDR r1, =__data_end
LDR r2, =__data_load
data_loop:
CMP r0, r1
ITTT LT
LDRLT r3, [r2], #4
STRLT r3, [r0], #4
BLT data_loop
; 4. 跳转到main()
BL main
; 如果main返回,进入死循环
B .
这里有个细节:__data_load 是Flash里的数据起始地址,__data_start 是RAM里的目标地址。链接脚本负责生成这些符号,启动代码负责搬运。
4.4 知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以把它当作启动代码的“路线图”:
4.5 实战中的几个坑
启动代码虽然短,但出问题往往很隐蔽。我总结几个常见陷阱:
- 向量表对齐:Cortex-M要求向量表按512字节对齐(
.align 9)。不对齐的话,硬件可能取错向量。 - 栈大小估算:
_estack不能随便给。我见过有人给栈分配了16字节,结果一调用函数就崩。建议至少256字节起步,复杂项目1KB以上。 - 中断优先级:复位后默认优先级可能不是你想要的。如果Bootloader里就开了中断,记得先设置优先级分组。
- 看门狗:有些芯片复位后看门狗默认开启。如果启动代码执行时间太长,看门狗会提前复位。我建议在启动代码最前面就喂一次狗,或者直接关掉它。
好了,启动代码的核心就这些。下一段代码里,你会看到它如何与链接脚本配合,把整个Bootloader的骨架搭起来。记住:启动代码是C语言世界的“开天辟地”,写稳了,后面就顺了。