4. 启动文件分析
任务目标
本案例以STM32F103系列(Cortex-M3)为例,分析其典型启动文件startup_stm32f10x_hd.s的核心结构,重点理解向量表定义与堆栈初始化过程。通过本案例,你将能够独立阅读任何基于GCC或ARMCC的Cortex-M启动文件,并准确指出复位后芯片执行的第一条指令路径。
启动文件核心结构
启动文件本质是一段汇编代码,在C语言main()执行前完成三件事:
- 定义初始堆栈指针(SP)值;
- 建立中断/异常向量表(含复位向量);
- 调用
SystemInit()与C库初始化后跳转main()。
以下代码为STM32F103启动文件的最小可复现等价实现(基于GCC汇编语法,已去除芯片厂商的冗余注释与宏封装),保留全部关键逻辑。
关键完整代码
.syntax unified
.cpu cortex-m3
.thumb
/* 栈大小定义(1KB,实际工程按需修改) */
.equ Stack_Size, 0x400
.section .stack, "w"
.align 3
_stack_top:
.space Stack_Size
.global _estack
_estack:
/* 堆大小定义(512B) */
.equ Heap_Size, 0x200
.section .heap, "w"
.align 3
_heap_start:
.space Heap_Size
_heap_end:
/* 向量表:必须位于0x00000000或0x08000000起始地址 */
.section .isr_vector, "a"
.align 9 /* 2^9 = 512字节对齐,满足Cortex-M向量表要求 */
.global g_pfnVectors
g_pfnVectors:
.word _estack /* 初始SP值 */
.word Reset_Handler /* 复位向量 */
.word NMI_Handler
.word HardFault_Handler
.word MemManage_Handler
.word BusFault_Handler
.word UsageFault_Handler
.word 0 /* 保留 */
.word 0
.word 0
.word 0
.word SVC_Handler
.word DebugMon_Handler
.word 0
.word PendSV_Handler
.word SysTick_Handler
/* 此处省略外部中断向量(EXTI0_IRQHandler等),实际工程按芯片型号补齐 */
/* 复位处理:芯片上电后第一条执行的代码 */
.section .text
.thumb_func
.global Reset_Handler
Reset_Handler:
ldr r0, =_estack
mov sp, r0 /* 冗余操作:硬件已加载SP,此处仅为显式确认 */
/* 复制.data段(已初始化全局变量)从Flash到RAM */
ldr r0, =_sdata
ldr r1, =_edata
ldr r2, =_sidata
copy_data:
cmp r0, r1
bge data_done
ldr r3, [r2], #4
str r3, [r0], #4
b copy_data
data_done:
/* 清零.bss段(未初始化全局变量) */
ldr r0, =_sbss
ldr r1, =_ebss
mov r2, #0
zero_bss:
cmp r0, r1
bge bss_done
str r2, [r0], #4
b zero_bss
bss_done:
/* 调用系统初始化与C库入口 */
bl SystemInit
bl __libc_init_array /* GCC工具链:C++全局构造与C库初始化 */
bl main
/* main返回后死循环(嵌入式设备不应返回) */
LoopForever:
b LoopForever
/* 默认异常处理:死循环(实际工程可在此添加调试断点) */
.thumb_func
.weak NMI_Handler
.weak HardFault_Handler
.weak MemManage_Handler
.weak BusFault_Handler
.weak UsageFault_Handler
.weak SVC_Handler
.weak DebugMon_Handler
.weak PendSV_Handler
.weak SysTick_Handler
Default_Handler:
b Default_Handler
/* 链接脚本需导出的符号(由.ld文件定义) */
.global _sdata
.global _edata
.global _sidata
.global _sbss
.global _ebss
.global SystemInit
.global main
向量表与堆栈初始化机制
向量表布局:Cortex-M3内核规定,Flash起始地址(0x08000000,映射到0x00000000)的第一个32位字必须为初始SP值,第二个32位字为复位后第一条指令地址。上电后硬件自动完成以下操作:
- 从地址0x00000000读取SP值并写入
sp寄存器; - 从地址0x00000004读取PC值并跳转执行。
堆栈初始化:_estack符号在链接时被解析为栈区最高地址(栈向下生长)。向量表首项.word _estack即完成SP初始化,无需软件干预。代码中Reset_Handler内的ldr/mov sp仅为防御性冗余,实际硬件已完成此操作。
运行与验证
环境依赖:需要arm-none-eabi-gcc工具链及配套链接脚本(.ld文件),该脚本需定义_sdata/_edata/_sidata/_sbss/_ebss等段符号,并将.isr_vector段放置在0x08000000起始位置。
编译命令:
arm-none-eabi-as -mcpu=cortex-m3 -mthumb startup.s -o startup.o
arm-none-eabi-ld -T link.ld startup.o -o firmware.elf
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
验证方法:
- 使用
arm-none-eabi-nm firmware.elf | grep _estack确认栈顶地址已解析; - 使用
arm-none-eabi-objdump -s -j .isr_vector firmware.elf查看向量表首两个32位字:第一个字应等于_estack地址值,第二个字应等于Reset_Handler地址值(且最低位为1表示Thumb模式); - 在调试器(OpenOCD + GDB)中单步执行,复位后PC应指向
Reset_Handler,SP值应等于_estack。
通过判定标准:若向量表首项SP值与链接脚本中RAM末端地址一致,且复位后调试器显示PC跳转至Reset_Handler标签处,则说明向量表与堆栈初始化正确。
常见问题
- 向量表首项不是栈顶:检查链接脚本中
_estack是否指向RAM最高地址,且.isr_vector段是否被放置在0x08000000起始处。 - 复位后PC跑飞:确认向量表中
Reset_Handler地址最低位为1(Thumb模式),否则硬件将进入Fault状态。 - 全局变量未初始化:检查
copy_data与zero_bss循环中使用的段符号是否在链接脚本中正确导出。