4. 启动文件分析

任务目标

本案例以STM32F103系列(Cortex-M3)为例,分析其典型启动文件startup_stm32f10x_hd.s的核心结构,重点理解向量表定义与堆栈初始化过程。通过本案例,你将能够独立阅读任何基于GCC或ARMCC的Cortex-M启动文件,并准确指出复位后芯片执行的第一条指令路径。

启动文件核心结构

启动文件本质是一段汇编代码,在C语言main()执行前完成三件事:

  1. 定义初始堆栈指针(SP)值;
  2. 建立中断/异常向量表(含复位向量);
  3. 调用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位字为复位后第一条指令地址。上电后硬件自动完成以下操作:

  1. 从地址0x00000000读取SP值并写入sp寄存器;
  2. 从地址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

验证方法:

  1. 使用arm-none-eabi-nm firmware.elf | grep _estack确认栈顶地址已解析;
  2. 使用arm-none-eabi-objdump -s -j .isr_vector firmware.elf查看向量表首两个32位字:第一个字应等于_estack地址值,第二个字应等于Reset_Handler地址值(且最低位为1表示Thumb模式);
  3. 在调试器(OpenOCD + GDB)中单步执行,复位后PC应指向Reset_Handler,SP值应等于_estack。

通过判定标准:若向量表首项SP值与链接脚本中RAM末端地址一致,且复位后调试器显示PC跳转至Reset_Handler标签处,则说明向量表与堆栈初始化正确。

Flash (0x08000000) .isr_vector 段 [0x00] 初始SP = _estack [0x04] Reset_Handler [0x08] NMI_Handler [0x0C] HardFault_Handler ... 其他异常向量 ... .text 段 Reset_Handler: 复制 .data → RAM 清零 .bss bl SystemInit → main RAM (0x20000000) .stack 段(向下生长) 高地址 _estack(栈顶) SP 初始值指向此处 .heap 段 .data(已初始化全局变量) 由Flash复制至此 .bss(未初始化全局变量) 启动时清零 硬件自动加载 SP ← [0x00] PC ← [0x04] 软件跳转

常见问题

  • 向量表首项不是栈顶:检查链接脚本中_estack是否指向RAM最高地址,且.isr_vector段是否被放置在0x08000000起始处。
  • 复位后PC跑飞:确认向量表中Reset_Handler地址最低位为1(Thumb模式),否则硬件将进入Fault状态。
  • 全局变量未初始化:检查copy_data与zero_bss循环中使用的段符号是否在链接脚本中正确导出。