本文将对STM32标准库中常见的启动文件 startup_stm32f10x_hd.s 进行逐行解析。这个文件是任何基于STM32F10x系列微控制器工程的基石,负责完成芯片上电后最关键的一系列初始化工作,最终引导至我们熟悉的C语言 main 函数。
启动文件使用的ARM汇编指令汇总
在深入代码之前,我们先熟悉一下文件中用到的主要ARM汇编指令。这些指令是理解启动流程的基础。

Stack——栈
栈是嵌入式系统运行不可或缺的部分,用于函数调用、局部变量和中断上下文保存等。启动文件的第一件事就是定义栈的大小和空间。
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
-
代码解读:
- 使用
EQU 伪指令定义了栈大小为 0x00000400,即 1KB。
AREA 指令开辟了一个名为 STACK 的段。NOINIT 表示不进行初始化,READWRITE 表示该段可读可写,ALIGN=3 指定按照 2^3 即 8 字节对齐。
SPACE 指令分配了大小为 Stack_Size 的连续内存空间。
- 标号
__initial_sp 紧跟在 SPACE 之后,它代表了栈的结束地址,也就是栈顶。在 ARM Cortex-M 架构中,栈是从高地址向低地址生长的。
-
要点提示:
- 栈的大小需要根据项目实际需求调整。如果程序复杂、函数调用层级深或局部变量多,可能导致栈溢出,引发难以调试的硬件错误(HardFault)。
EQU 的作用类似于 C 语言中的 #define,用于定义常量。
Heap——堆
堆主要用于动态内存分配,例如 C 标准库中的 malloc 函数。在资源受限的单片机开发中,动态内存使用相对较少,但启动文件依然为其预留了空间。

Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
- 代码解读:
- 堆大小被定义为
0x00000200,即 512 字节。
- 同样使用
AREA 开辟了一个名为 HEAP 的段,属性与栈段类似。
__heap_base 是堆的起始地址,__heap_limit 是堆的结束地址。与栈相反,堆是从低地址向高地址生长的。
PRESERVE8
THUMB
PRESERVE8:指示编译器保持 8 字节的栈对齐。
THUMB:声明后续指令使用 Thumb 指令集。现代的 Cortex-M 系列内核均使用 Thumb-2 指令集,它兼容 16 位和 32 位指令。
向量表
向量表是整个启动过程中最核心的数据结构之一。它本质上是一个存储在 Flash 起始地址(0x0800 0000)的地址数组。当发生异常或中断时,处理器内核会通过查询这张表,快速跳转到对应的服务程序入口。
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
- 代码解读:
AREA RESET, DATA, READONLY 定义了一个只读的数据段,名为 RESET。这个段通常被链接器定位到存储器的起始位置。
EXPORT 关键字将 __Vectors、__Vectors_End 和 __Vectors_Size 这三个符号声明为全局(global),使得它们可以被其他链接文件(如链接脚本)或 C 代码引用。
向量表的工作原理:
内核通过一个可重定位的寄存器来定位向量表。复位后,该寄存器默认为 0,因此处理器从地址 0(在单片机中,通常映射到 Flash 的起始地址)开始查找向量表。向量表的第一个条目(地址 0x0000 0000)比较特殊,它存放的是初始主栈指针(MSP)的值,而非一个函数地址。从第二个条目开始,才依次存放各个异常和中断的服务程序入口地址。
下图展示了 STM32F103 的部分向量表内容:

__Vectors DCD __initial_sp ; 栈顶地址
DCD Reset_Handler ; 复位中断服务程序地址
DCD NMI_Handler ; 不可屏蔽中断
DCD HardFault_Handler ; 硬件错误中断
DCD MemManage_Handler ; [存储器管理](https://yunpan.plus/f/36-1)错误
DCD BusFault_Handler ; 总线错误
DCD UsageFault_Handler ; 用法错误(如非法指令)
DCD 0 ; 保留
DCD 0 ; 保留
DCD 0 ; 保留
DCD 0 ; 保留
DCD SVC_Handler ; 系统服务调用
DCD DebugMon_Handler ; 调试监控
DCD 0 ; 保留
DCD PendSV_Handler ; 可挂起的系统服务
DCD SysTick_Handler ; 系统节拍定时器中断
; 外部中断开始
DCD WWDG_IRQHandler
DCD PVD_IRQHandler
DCD TAMPER_IRQHandler
; 限于篇幅,中间代码省略...
DCD DMA2_Channel2_IRQHandler
DCD DMA2_Channel3_IRQHandler
DCD DMA2_Channel4_5_IRQHandler
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
- 代码解读:
__Vectors 是向量表的起始标签。
DCD 指令用于分配一个或多个字(Word,4字节)的内存空间,并以指定的数据(这里是函数标签代表的地址)初始化它们。
- 向量表按顺序包含了所有系统异常和外部中断的入口地址。C语言中的函数名本身就是一个地址,因此这里直接使用函数名。
__Vectors_End 标记向量表结束,__Vectors_Size 通过计算得出向量表的总大小。
复位程序
系统上电或按下复位键后,第一个执行的代码就是复位中断服务程序 Reset_Handler。它是引导程序进入 C 世界的“桥梁”。
AREA |.text|, CODE, READONLY
首先定义了一个名为 .text 的只读代码段,接下来的代码都将放在这个段中。

Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
- 代码解读:
PROC 和 ENDP 用于定义一个子程序的开始和结束。
EXPORT ... [WEAK]:将 Reset_Handler 声明为弱符号。这意味着如果用户在工程的其他地方(例如另一个汇编或 C 文件)重新定义了一个同名的强符号 Reset_Handler,链接器将使用用户定义的版本。这提供了覆盖默认启动代码的灵活性。
IMPORT:声明 SystemInit 和 __main 这两个符号来自外部文件,类似于 C 语言中的 extern。
LDR R0, =SystemInit:将 SystemInit 函数的地址加载到寄存器 R0。
BLX R0:跳转到 R0 寄存器的地址(即执行 SystemInit 函数),并将返回地址保存到链接寄存器 LR。SystemInit() 是标准库函数,通常位于 system_stm32f10x.c 中,主要负责配置系统时钟(例如将主频设置为 72MHz)。
LDR R0, =__main:将 __main 的地址加载到 R0。注意:此 __main 并非我们写的 C 语言 main 函数,它是 C 库函数,负责初始化用户堆栈、全局变量等(即运行时的环境搭建),最后才会调用我们编写的 main 函数。
BX R0:跳转到 __main,正式进入 C 语言世界。
相关指令说明:
下表简单说明了复位程序中用到的几条 ARM 指令:

中断服务程序
启动文件为所有可能的异常和中断都预先定义了一个空的服务程序。这样做是为了防止因为程序员遗漏某个中断服务程序,而导致中断发生时程序“跑飞”。
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ENDP
; 限于篇幅,中间系统异常处理程序省略...
SysTick_Handler PROC
EXPORT SysTick_Handler [WEAK]
B .
ENDP
Default_Handler PROC
EXPORT WWDG_IRQHandler [WEAK]
EXPORT PVD_IRQHandler [WEAK]
EXPORT TAMP_STAMP_IRQHandler [WEAK]
; 限于篇幅,中间外部中断处理程序省略...
EXPORT LTDC_IRQHandler [WEAK]
EXPORT LTDC_ER_IRQHandler [WEAK]
EXPORT DMA2D_IRQHandler [WEAK]
B .
ENDP
- 代码解读:
- 所有中断处理程序都被声明为
[WEAK] 弱符号。
- 函数体内只有一条指令:
B .。这是一个无条件跳转指令,跳转到当前地址(‘.’表示当前地址),即构成一个死循环。
- 当你在 C 代码中正确定义了例如
void USART1_IRQHandler(void) {...} 函数后,由于你定义的符号是“强”的,链接器就会使用你的函数地址来填充向量表中的对应条目,而忽略启动文件中这个弱的空函数。
- 如果开启了某个中断(例如 USART1 接收中断)却没有编写服务函数,中断触发后程序就会跳转到这个空的
Default_Handler 中的死循环,造成程序“卡死”的现象。这在调试时是一个重要的线索。
用户堆栈初始化
最后这部分代码决定了如何初始化堆和栈的起始地址,它区分了两种 C 库环境:微库 (MicroLIB) 和标准 C 库。
ALIGN
IF :DEF:__MICROLIB
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit
ELSE
IMPORT __use_two_region_memory
EXPORT __user_initial_stackheap
__user_initial_stackheap
LDR R0, = Heap_Mem
LDR R1, =(Stack_Mem + Stack_Size)
LDR R2, = (Heap_Mem + Heap_Size)
LDR R3, = Stack_Mem
BX LR
ALIGN
ENDIF
END
-
代码解读:
ALIGN 用于地址对齐。
IF :DEF:__MICROLIB:这是一个条件编译指令,检查是否定义了 __MICROLIB 宏。这个宏通常在 IDE(如 Keil MDK)的工程配置中设置。
情况一:使用 MicroLIB
- 如果定义了
__MICROLIB,则仅将栈顶 (__initial_sp)、堆起始 (__heap_base) 和堆结束 (__heap_limit) 的地址导出为全局符号。堆栈的初始化工作将由更精简的 MicroLIB 中的 __main 函数负责。
情况二:使用标准 C 库
- 如果未使用 MicroLIB,则采用双区存储器模型,并导出一个名为
__user_initial_stackheap 的函数。
- 这个函数需要由 C 库调用,它通过设置 R0-R3 寄存器来返回堆和栈的地址信息:
- R0: 堆的起始地址 (
Heap_Mem)
- R1: 栈的结束地址,即初始栈顶 (
Stack_Mem + Stack_Size)
- R2: 堆的结束地址 (
Heap_Mem + Heap_Size)
- R3: 栈的起始地址 (
Stack_Mem)
- 函数最后
BX LR 返回到调用者。
如何在 Keil 中配置 MicroLIB?
下图展示了在 Keil MDK 的 Options for Target 对话框中,勾选 Use MicroLIB 选项的位置。

其他注意事项:
IF, ELSE, ENDIF 是汇编器的条件编译指令,逻辑与 C 语言中的 #ifdef 类似。
END 指令标记整个汇编文件的结束。
总结与建议
通过对 startup_stm32f10x_hd.s 文件的剖析,我们清晰地看到了一个STM32芯片从上电到执行 main 函数所经历的完整路径:设置堆栈、建立中断向量表、执行时钟初始化、最后搭建C语言运行环境。理解这个过程,对于深入掌握嵌入式系统底层原理、调试启动相关故障(如堆栈溢出、中断未响应)至关重要。
在实际项目中,如果你需要特殊的初始化操作(例如在调用 main 前初始化某些外设),可以考虑修改或重写 Reset_Handler 函数。同时,务必根据应用程序的复杂度合理设置 Stack_Size 和 Heap_Size,避免内存相关错误。
希望这篇详细的解析能帮助你更好地驾驭STM32的启动过程。如果你在嵌入式开发中遇到其他底层问题,欢迎在云栈社区与其他开发者交流探讨。