嵌入式:启动汇编与 C 的交接
更新时间:2026-09-21。嵌入式/裸机视角。它依据 链接脚本入门 里的符号完成内存初始化。
一句话回答
复位后硬件先从向量表第一项取出初始栈指针、第二项取出复位入口并跳转;这段 reset 代码(通常是汇编/极简 C)先设好栈,再把 .data 初值从 Flash 拷到 RAM、把 .bss 清零、做时钟等基础初始化,等 C 运行环境就绪后才 bl main——在栈和变量没准备好之前直接跑 C 会崩。
一、启动的完整顺序
- 取栈指针:内核把向量表第 0 项载入 MSP(
_estack); - 跳 Reset_Handler:向量表第 1 项是复位入口;
- 拷
.data:从 Flash 的 LMA 把有初值的全局变量拷到 RAM 的 VMA; - 清
.bss:把零初始化变量区域写 0; - 系统初始化:配时钟(SystemInit)、必要的全局/静态构造(C++);
- 调用
main:此时 C 环境已完整; - main 返回后通常进死循环(裸机没有 OS 可退回)。
二、典型 startup(GCC 汇编示意)
asm
.section .isr_vector,"a",%progbits
vector_table:
.word _estack /* 第 0 项:初始 MSP */
.word Reset_Handler /* 第 1 项:复位入口 */
/* ...其余中断向量,默认指向死循环 */
.section .text.Reset_Handler
Reset_Handler:
ldr r0, =_sdata /* 拷贝 .data */
ldr r1, =_edata
ldr r2, =_sidata
1: cmp r0, r1
bcc 2f
b 3f
2: ldr r4, [r2], #4
str r4, [r0], #4
b 1b
3:
ldr r0, =_sbss /* 清零 .bss */
ldr r1, =_ebss
movs r2, #0
4: cmp r0, r1
bcc 5f
b 6f
5: str r2, [r0], #4
b 4b
6:
bl SystemInit /* 时钟等初始化 */
bl main /* 进入 C 世界 */
loop:
b loop /* main 不应返回 */(不同厂商 startup 文件把这些写成更紧凑的循环,原理相同。)
三、为什么开头必须是汇编
- C 函数调用依赖栈,而 SP 在复位瞬间才由向量表建立;
- C 代码读到的全局变量要靠
.data/.bss初始化,这只能由不依赖这些变量的底层代码先完成; - 向量表布局是硬件约定,必须精确放到 Flash 起始并用
KEEP保留。
常见坑
- 没拷 .data / 没清 .bss:全局变量初值随机、零值变量非零,程序表现诡异。
- SP 指向错误:
_estack算错或向量表没在 Flash 起点,一进函数就跑飞。 - 向量表偏移未设置:用 bootloader 或向量表不在 0x08000000 时,需设置 SCB->VTOR。
- 指望 main 返回:裸机无返回处,startup 末尾应留死循环。
- 进 reset 就调用复杂 C 函数:环境未就绪,应先完成搬运清零再调用。
总结
启动流程是「向量表装载 SP → Reset_Handler 拷 .data、清 .bss、初始化时钟 → 跳 main」,由汇编或极简 C 完成,因为栈和已初始化变量就绪前无法安全运行普通 C 代码;向量表位置、SP 和三段内存初始化任一出错,程序都会在进入 main 前崩溃。