Hotdry.

Article

STM32裸机启动:向量表重映射与链接脚本内存布局实战

深入解析Cortex-M裸机启动流程:向量表结构与放置、链接脚本MEMORY/SECTIONS配置、数据段VMA/LMA分离、Reset_Handler中.data复制与.bss清零的完整实现与调试要点。

2026-05-15systems

在 STM32 开发中,使用 HAL 或 CubeMX 生成的启动代码虽然方便,却也隐藏了芯片上电后的关键初始化步骤。当我们需要理解复位序列、调试 HardFault 或实现 bootloader 时,手写向量表与链接脚本就成了必修课。本文基于 Cortex-M4(STM32F446RE)与 Cortex-M0(STM32F031K6)两个平台,梳理从向量表放置到内存布局的完整链路,并给出调试时排查问题的 checklist。

Cortex-M 复位序列与向量表结构

STM32 基于 ARM Cortex-M 架构,上电后的启动行为由硬件固定:

  1. 复位后,处理器从地址0x00000000读取初始 SP 值
  2. 再从地址0x00000004读取初始 PC 值(复位向量)
  3. 实际芯片将0x08000000(Flash)和0x20000000(SRAM)映射到0x00000000区域

因此,向量表必须位于 Flash 起始地址0x08000000,且布局严格遵循以下顺序:

偏移 名称 说明
0x00 初始 SP _estack,SRAM 末尾
0x04 复位向量 Reset_Handler地址
0x08 NMI 不可屏蔽中断
0x0C HardFault 硬件错误

Cortex-M0 最多有 48 个中断条目,Cortex-M4 更多。典型实现使用.word伪指令按顺序排列,条目不足时填0。每个处理器的中断数不同,可参考 ST 官方启动文件(如startup_stm32f031x6.s)获取完整列表。

链接脚本:MEMORY 与 SECTIONS 的配置逻辑

链接脚本(.ld文件)本质上是与链接器的一份合同,描述三件事:入口点、可用内存、将哪些 section 放到哪里。

MEMORY 块:定义物理地址空间

MEMORY
{
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
    SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

这里定义了两个区域:

  • FLASH:可读可执行(rx),程序代码与常量存放于此
  • SRAM:可读可写(rwx),运行时变量与栈空间

对于不同型号芯片需查对应参考手册确认实际大小,例如 STM32F031K6 只有 32KB Flash 和 4KB SRAM。

SECTIONS 块:section 映射规则

SECTIONS {
    .text : {
        . = ALIGN(4);
        *(.isr_vector_table)   /* 向量表必须排第一 */
        *(.text) *(.text*) *(.rodata*)
        . = ALIGN(4);
        _etext = .;
    } > FLASH

    .data : {
        . = ALIGN(4);
        _sdata = .;
        *(.data)
        . = ALIGN(4);
        _edata = .;
    } > SRAM AT > FLASH   /* 关键:VMA在SRAM,LMA在FLASH */

    .bss : {
        . = ALIGN(4);
        _sbss = .;
        *(.bss) *(COMMON)
        . = ALIGN(4);
        _ebss = .;
    } > SRAM
}

VMA 与 LMA:理解数据段的双地址问题

初始化数据(如static int count = 42;)面临一个矛盾:它们的初始值需要在 Flash 中存储(掉电不丢失),但运行时必须位于 SRAM 中供 CPU 读 / 写。

  • VMA(Virtual Memory Address):运行时地址,即&count返回的地址,必须在 SRAM
  • LMA(Load Memory Address):存储地址,初始值存放在 Flash

AT > FLASH语法告诉链接器:.data段的 VMA 在 SRAM,但把初始值复制到 Flash 对应位置。链接器会生成符号_etext,标记 Flash 中.data初始值的末尾 —— 这是后续启动代码需要读取的位置。

必需符号定义

链接脚本中应导出以下符号供启动代码使用:

_estack  = ORIGIN(SRAM) + LENGTH(SRAM);  /* 栈顶,SRAM末尾 */
_sidata  = LOADADDR(.data);              /* .data初始值在Flash的起始地址 */
_sdata   = ADDR(.data);                  /* .data在SRAM的起始地址 */
_edata   = ADDR(.data) + SIZEOF(.data);  /* .data在SRAM的结束地址 */
_sbss    = ADDR(.bss);
_ebss    = ADDR(.bss) + SIZEOF(.bss);

Reset_Handler:从零初始化 C 运行时环境

启动代码的核心任务是Reset_Handler,它在上电后首先运行,负责:

  1. 设置栈指针
  2. .data从 Flash 复制到 SRAM
  3. .bss清零
  4. 调用main()

C 语言实现的 Reset_Handler

#include <stdint.h>

extern uint32_t _estack;
extern uint32_t _etext;
extern uint32_t _sdata;
extern uint32_t _edata;
extern uint32_t _sbss;
extern uint32_t _ebss;

void Reset_Handler(void) {
    uint32_t data_size = ((uint32_t)&_edata - (uint32_t)&_sdata) / 4;
    uint32_t bss_size   = ((uint32_t)&_ebss - (uint32_t)&_sbss) / 4;

    /* 复制.data段:从Flash(_etext)到SRAM(_sdata) */
    uint32_t *src = (uint32_t *)&_etext;
    uint32_t *dst = (uint32_t *)&_sdata;
    for (uint32_t i = 0; i < data_size; i++) {
        *dst++ = *src++;
    }

    /* 清零.bss段 */
    dst = (uint32_t *)&_sbss;
    for (uint32_t i = 0; i < bss_size; i++) {
        *dst++ = 0;
    }

    main();
    while (1);  /* main返回则死循环 */
}

/4是因为操作单位是uint32_t(4 字节)。如果忘记除以 4,复制量会是实际需要的 4 倍,可能导致踩内存。

汇编实现的 Reset_Handler(Cortex-M0 示例)

reset_handler:
    LDR  r0, =_estack
    MOV  sp, r0

    /* 复制.data段 */
    MOVS r0, #0
    LDR  r1, =_sdata
    LDR  r2, =_edata
    LDR  r3, =_sidata
    B    copy_loop

copy_sidata:
    LDR  r4, [r3, r0]
    STR  r4, [r1, r0]
    ADDS r0, r0, #4

copy_loop:
    ADDS r4, r0, r1
    CMP  r4, r2
    BCC  copy_sidata

    /* 清零.bss段 */
    MOVS r0, #0
    LDR  r1, =_sbss
    LDR  r2, =_ebss
    B    bss_loop

reset_bss:
    STR  r0, [r1]
    ADDS r1, r1, #4

bss_loop:
    CMP  r1, r2
    BCC  reset_bss

    B    main

两种实现效果相同,C 版本可读性更好,但需要__attribute__((section))等机制配合编译器。

向量表定义:section 属性与 weak 别名

向量表本身是一个数组,第一个元素是_estack(初始 SP),第二个是Reset_Handler

uint32_t vector_table[] __attribute__((section(".isr_vector_table"))) = {
    (uint32_t)&_estack,
    (uint32_t)&Reset_Handler,
    (uint32_t)&NMI_Handler,
    (uint32_t)&HardFault_Handler,
    /* ... 其余中断向量 ... */
};

.isr_vector_table这个 section 名称必须与链接脚本中完全一致。常见错误是写成.isr_vector.vector_table,导致链接器找不到匹配输入而创建孤立输出 section。

中断处理函数的 weak 别名模式

void Default_Handler(void) {
    while (1) { }  /* 未配置的中断默认进入死循环 */
}

void NMI_Handler(void) __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));

weak属性使链接时若存在同名强定义则覆盖默认实现,alias则将函数指针指向同一个目标函数。这保证所有中断都有有效处理器,避免上电后进入未定义指令。

调试清单:用 objdump 验证内存布局

链接完成后,使用arm-none-eabi-objdump -h <elf>检查 section 分布。第一列 VMA(Virtual Memory Address)必须是:

  • .text(含向量表):0x08000000(Flash 起始)
  • .data0x20000000(SRAM 起始)
  • .bss0x20000000之后的某个位置

常见错误场景

向量表在 SRAM 中:VMA 显示.isr_vector_table0x20000000,意味着它未被正确合并到.text中。原因通常是 section 名称不匹配。

向量表偏移:向量表虽然也在 Flash 中,但不在起始位置。可能的原因是指令排列顺序导致链接器将其他内容排在前面。

HardFault:上电后立即进入 HardFault(xPSR 显示0x01000003),通常是向量表位置错误导致 SP/PC 读取了垃圾值。

修复后,objdump 输出应类似:

Idx Name          Size      VMA       LMA
  0 .text         000000fc  08000000  08000000
  1 .data         00000000  20000000  080000fc
  2 .bss          00000000  20000000  080000fc

.text0x08000000开始,大小包含向量表;.data.bss的 VMA 在 SRAM,LMA 在 Flash 对应位置。

构建与烧录

# 编译(-ffreestanding:不依赖标准库)
arm-none-eabi-gcc -c -mcpu=cortex-m4 -mthumb -std=gnu23 startup.c -o startup.o -ffreestanding
arm-none-eabi-gcc -c -mcpu=cortex-m4 -mthumb -std=gnu23 main.c -o main.o -ffreestanding

# 链接(-nostdlib:禁止链接标准库)
arm-none-eabi-gcc -nostdlib -mcpu=cortex-m4 -mthumb -T stm32f446.ld *.o -o firmware.elf

# 验证布局
arm-none-eabi-objdump -h firmware.elf

# 烧录(OpenOCD + GDB)
openocd -f board/st_nucleo_f4.cfg
# GDB中
monitor flash write_image erase firmware.elf
monitor resume

核心参数速查

参数 STM32F446RE STM32F031K6
Flash 起始 0x08000000 0x08000000
SRAM 起始 0x20000000 0x20000000
Flash 大小 512KB 32KB
SRAM 大小 128KB 4KB
_estack 0x20020000 0x20001000
编译器 arm-none-eabi-gcc arm-none-eabi-gcc
目标架构 cortex-m4 cortex-m0

关键工程要点

链接脚本是契约:启动代码中的 section 名称必须与链接脚本完全一致,字符差异会导致向量表进入错误内存区域。

objdump 优先于调试器:烧录前先检查 section 地址,可提前发现向量表错位问题,避免在 HardFault 中浪费时间。

VMA/LMA 分离是初始化数据的标准模式:理解> SRAM AT > FLASH语法,才能正确处理带初始值的全局变量。

weak 别名保证健壮性:所有未实现的中断处理器默认进入死循环,优于运行到未知地址。

资料来源:magdaref.com(STM32 bare metal: writing a linker script and startup code from scratch)、vivonomicon.com(Bare Metal STM32 Programming Part 2)

systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com