第四章:汇编基础与启动代码
ARM/Thumb指令集入门
说到Bootloader,绕不开的就是汇编。很多做嵌入式开发的朋友,一听到汇编就头疼。我刚开始也是这样,觉得C语言多方便啊,干嘛要折腾汇编?
但后来我明白了。芯片上电那一刻,C语言环境还没建立起来。没有堆栈,没有全局变量初始化,你让main函数怎么跑?这时候,就得靠汇编来打头阵。
ARM处理器支持两种指令集:ARM指令集和Thumb指令集。说白了,ARM指令是32位的,每条指令能干更多事,但占空间大。Thumb指令是16位的,代码密度高,适合对Flash容量敏感的场景。
核心区别:
- ARM指令:32位,功能强,性能高
- Thumb指令:16位,代码紧凑,适合低成本MCU
- Thumb-2指令:混合16/32位,现代Cortex-M系列用的就是它
我个人习惯在启动代码里用ARM指令集。为什么?因为启动代码就那么几十行,不差这点空间,但ARM指令的可读性和调试便利性更好。你想想看,万一芯片起不来,用调试器看反汇编,ARM指令比Thumb好认多了。
汇编启动代码(Reset_Handler)
每个ARM芯片上电后,第一个执行的代码就是Reset_Handler。这个函数名是固定的,写在中断向量表的第一项。我在项目中遇到过好几次,有人把函数名拼错了,结果芯片一直跑飞——嗯,这种坑踩过一次就记住了。
一个典型的Reset_Handler长这样:
Reset_Handler PROC
EXPORT Reset_Handler
; 设置堆栈指针
LDR SP, =_estack
; 复制.data段到SRAM
BL __data_init
; 清零.bss段
BL __bss_init
; 跳转到C语言main函数
BL main
; 如果main返回,死循环
B .
ENDP
这里有几个关键点:
- EXPORT:把Reset_Handler符号导出,链接器才能找到它
- LDR SP, =_estack:加载栈顶地址到SP寄存器
- BL:带链接的跳转,相当于C语言的函数调用
- B .:无限循环,防止程序跑飞
避坑指南:
我曾经在某个项目里,忘记在启动代码最后加死循环。结果main函数返回后,PC指针乱跳,系统直接崩溃。从那以后,我每次写启动代码都会检查这个细节。
设置堆栈指针(SP)
堆栈指针SP,是C语言运行的基础。没有它,局部变量、函数调用、中断嵌套全都玩不转。
设置SP有两种方式:
| 方式 | 代码 | 说明 |
|---|---|---|
| 直接加载 | LDR SP, =_estack |
从链接脚本定义的符号加载栈顶地址 |
| 从向量表加载 | LDR SP, [R0] |
Cortex-M系列自动从0地址加载SP |
Cortex-M系列比较特殊。芯片上电后,硬件会自动从地址0x00000000加载SP值,从地址0x00000004加载PC值。所以对于Cortex-M,你甚至不需要在启动代码里显式设置SP——硬件帮你做了。
但如果是Cortex-A/R系列,或者老款的ARM7/ARM9,你就得手动设置SP。我记得有一次调试一块ARM9的板子,SP没设对,一调用函数就死机。查了半天,发现是栈顶地址算错了,少算了一个0。你说冤不冤?
注意事项:
堆栈是向下生长的。也就是说,SP初始值要指向SRAM的最高地址。比如你的SRAM是0x20000000到0x20010000,那SP应该设为0x20010000。设错了,栈溢出时会覆盖你的全局变量,这种bug极难排查。
跳转到C语言main函数
设置好SP,初始化好.data和.bss段,接下来就是重头戏——跳转到main函数。
这里用BL指令而不是B指令,是有讲究的:
- BL main:会保存返回地址到LR寄存器。如果main函数返回,还能回到启动代码
- B main:直接跳转,不保存返回地址。main函数返回后,程序就失控了
我个人建议用BL。虽然大多数嵌入式系统的main函数不会返回,但万一呢?而且用BL更符合函数调用的语义,代码可读性更好。
跳转之前,别忘了初始化C语言运行环境。具体来说:
- 初始化.data段:把Flash里的初始值复制到SRAM
- 清零.bss段:把未初始化的全局变量清零
- 设置堆栈指针:确保函数调用有栈可用
这三个步骤缺一不可。我曾经见过一个项目,为了省事跳过了.bss段清零,结果全局变量默认值随机,程序行为完全不可预测。调试了整整两天才发现是这个问题。
总结一下启动流程:
上电 → 硬件设置PC到Reset_Handler → 设置SP → 初始化.data/.bss → 跳转main → 死循环
就这么简单,但每一步都不能错。
嗯,说到这里,我想起一个有意思的事。有次给客户做培训,有个工程师问我:为什么启动代码要用汇编,不能用C写?
答案其实很简单:C语言本身依赖堆栈和全局变量。在堆栈和全局变量还没初始化好的时候,你连一个简单的函数调用都做不了。这就叫「先有鸡还是先有蛋」的问题。汇编就是那只鸡,它不需要任何前提条件就能运行。
所以,别嫌汇编麻烦。它是Bootloader的基石,也是你理解嵌入式系统运行原理的关键。把这章吃透了,后面的内容就会顺畅很多。