找回密码
立即注册
搜索
发回帖 发新帖

6443

积分

0

好友

819

主题
发表于 昨天 23:01 | 查看: 8| 回复: 0

编译报 "region FLASH overflowed" 还能理解,但全局变量加着加着程序就跑飞、某个数组初值明明写了却不生效——这些诡异问题的根子,多半出在内存布局和链接脚本上。

四个区域与两块物理存储

C 程序编译后的内容按属性分成几段,链接器把段映射到物理存储器上。

段 内容 物理位置 运行时是否可写
.text 函数代码、中断向量表 Flash 否
.rodata 字符串常量、const 全局量、查表数据 Flash 否
.data 有初值的全局/静态变量 Flash 存副本,运行时在 RAM 是
.bss 无初值或初值为 0 的全局/静态变量 仅 RAM,Flash 只存大小信息 是
stack 局部变量、函数调用现场 RAM 是
heap malloc 分配区 RAM 是

.data 这一行是理解内存布局的关键:它在 Flash 里也占一份空间。上电时变量初值从 Flash 拷贝到 RAM,所以一个 4KB 的全局数组会同时吃掉 4KB Flash 和 4KB RAM。.bss 不占 Flash,因为初始值全是 0,启动时清零即可。

局部变量在栈上,占用的是运行时的 RAM,不体现在 Flash 里。反过来,函数里写的 static 变量落在 .data 或 .bss,与其他全局变量没有区别。

启动代码做了什么

很多人以为全局变量的初值是编译器自动生效的,实际上这是启动代码在 main() 之前完成的。Cortex-M 的复位流程大致是:先设置主栈指针 MSP,接着把 .data 段从 Flash 拷贝到 RAM,再把 .bss 段全部清零,最后调用 SystemInit 和 __libc_init_array,才进入 main()。

对应的汇编启动代码大致是:

Reset_Handler:
    ldr   r0, =_estack
    msr   msp, r0              /* 主栈指针 */

    /* 拷贝 .data: Flash 的 _sidata -> RAM 的 _sdata */
    ldr   r0, =_sdata
    ldr   r1, =_edata
    ldr   r2, =_sidata
    movs  r3, #0
    b     LoopCopyDataInit
CopyDataInit:
    ldr   r4, [r2, r3]
    str   r4, [r0, r3]
    adds  r3, r3, #4
LoopCopyDataInit:
    adds  r4, r0, r3
    cmp   r4, r1
    bcc   CopyDataInit

    /* 清零 .bss */
    ldr   r2, =_sbss
    ldr   r4, =_ebss
    movs  r3, #0
    b     LoopFillZerobss
FillZerobss:
    str   r3, [r2]
    adds  r2, r2, #4
LoopFillZerobss:
    cmp   r2, r4
    bcc   FillZerobss

    bl    SystemInit
    bl    __libc_init_array
    bl    main

_sdata、_sidata、_ebss 这些符号不是编译器自动生成的,是链接脚本定义的。改链接脚本时把 .data 的加载地址写错,表现就是全局变量初值全乱——程序能跑,但参数全是随机数。

链接脚本的两个地址

理解 LMA(加载地址)和 VMA(虚拟地址/运行地址) 之后,链接脚本就不再是天书。

  • LMA:这段代码或数据“存放”在哪里,即烧录位置。
  • VMA:运行时“运行”在哪里。

.text 和 .rodata 的 LMA 与 VMA 都在 Flash,两者相同。.data 的 LMA 在 Flash、VMA 在 RAM,所以要用 AT() 显式指定加载地址。

MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
    CCMRAM(rw)  : ORIGIN = 0x10000000, LENGTH = 64K
}

SECTIONS
{
    /* 中断向量表必须放在 Flash 起始位置 */
    .isr_vector :
    {
        . = ALIGN(4);
        KEEP(*(.isr_vector))
        . = ALIGN(4);
    } >FLASH

    .text :
    {
        . = ALIGN(4);
        *(.text)
        *(.text*)
        *(.rodata)
        *(.rodata*)
        _etext = .;          /* .data 的加载地址从这里开始 */
    } >FLASH

    /* .data: 运行地址在 RAM,加载地址紧跟在 .text 之后 */
    .data : AT (_etext)
    {
        . = ALIGN(4);
        _sdata = .;
        *(.data)
        *(.data*)
        . = ALIGN(4);
        _edata = .;
    } >RAM

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

    /* 栈顶与堆的空间检查 */
    ._user_heap_stack :
    {
        . = ALIGN(8);
        PROVIDE ( end = . );
        PROVIDE ( _end = . );
        . = . + _Min_Heap_Size;
        . = . + _Min_Stack_Size;
        . = ALIGN(8);
    } >RAM
}

_sidata 在多数 STM32 工程里被定义为 LOADADDR(.data),等价于紧跟 _etext。链接器会保证 AT() 指定的位置不与前面的段重叠。

查看真实占用

size 命令给出三列数字,含义经常被人读错:

arm-none-eabi-size -A build/firmware.elf
section              size        addr
.isr_vector           392   134217728
.text               42108   134218120
.rodata               316   134260228
.data                 512   536870912
.bss                 8424   536871424
  • text 列 = 代码 + 只读数据,占用 Flash
  • data 列 = 有初值的全局变量,Flash 和 RAM 各占一份
  • bss 列 = 只占 RAM

Flash 实际占用 = text + data,RAM 实际占用 = data + bss + 栈 + 堆。很多“RAM 够用”的误判都来自漏算了栈和堆。

要找具体哪个符号占得最多,看 map 文件:

arm-none-eabi-nm --print-size --size-sort -r build/firmware.elf | head -20

输出按大小降序列出符号,一眼能看出是某个查表数组、某个任务栈还是某个缓冲区在吃 RAM。map 文件里还有 Cross Reference Table 和每个目标文件的段占用统计,排查“为什么加了这个功能 RAM 就爆了”时很有用。

把东西放到指定位置

把关键数据放到特定内存区,靠的是自定义段加链接脚本配合。

// 定义到 CCM RAM(Cortex-M4 的 64KB 紧耦合内存,CPU 访问零等待)
__attribute__((section(".ccmram"))) uint8_t g_fast_buf[4096];

// 定义到指定对齐的 DMA 缓冲,避免缓存行与对齐问题
__attribute__((section(".dma_buf"), aligned(32))) uint8_t g_dma_rx[1024];

链接脚本里要声明这些段:

    .ccmram (NOLOAD) :
    {
        . = ALIGN(4);
        _sccmram = .;
        *(.ccmram)
        *(.ccmram*)
        . = ALIGN(4);
        _eccmram = .;
    } >CCMRAM

    .dma_buf (NOLOAD) :
    {
        . = ALIGN(32);
        *(.dma_buf)
    } >RAM

用 CCM RAM 时注意一条限制:它通常不在 DMA 能访问的总线上,DMA 缓冲放进去会收不到数据。外部 SDRAM 也有类似问题——能 DMA 但访问延迟大,且需要先初始化控制器才能用,.data 段不能放进去(启动时还没初始化)。

常用的特殊区域对比:

区域 特点 注意
主 SRAM 通用,DMA 可访问 带 D-Cache 的核需注意一致性
CCM RAM CPU 独占,零等待,不占用总线带宽 通常不可 DMA
TCM(M7) 指令/数据紧耦合,延迟确定 容量小,需显式放置
Backup SRAM 低功耗域保持 需开启备份域电源与时钟
外部 SDRAM 容量大 需初始化控制器,不适合作启动段

栈与堆的设置

栈大小由链接脚本里的 _Min_Stack_Size 提供(裸机工程),或者由 RTOS 为每个任务单独指定。栈从 RAM 高地址向下生长,堆从 end 符号向上生长,两者之间是“谁先撞上谁”的关系。

裸机工程的栈估算方法是算最坏调用链:每个函数的局部变量加现场保存(Cortex-M 约 64~100 字节每级,带浮点更多),乘上嵌套深度,再加上中断嵌套的余量。查实际水位的方法是把栈区填充成固定图案(如 0xCD),运行一段时间后从栈顶往下数未被改写的字节数。

RTOS 环境下要区分三块内存:

RAM 总占用 = .data + .bss + 系统栈(中断与启动用)
           + 各任务栈之和 + RTOS 内核对象(TCB/队列/信号量)
           + heap_4 管理的堆(若用动态分配)

FreeRTOS 的 configTOTAL_HEAP_SIZE 是从一个静态数组里划出来的,这个数组本身在 .bss 里。任务栈从这个堆分配,所以调大 configTOTAL_HEAP_SIZE 直接体现为 bss 增长,而不是运行时动态增长。

malloc 走的是 newlib 的 _sbrk,从 end 符号之后的堆区分配。裸机工程里 _Min_Heap_Size 设得过小(有些模板默认 0x200),malloc 会在某个时刻静默失败,返回值是 NULL 却没被检查。

Bootloader 场景下的地址偏移

带 Bootloader 时,APP 的链接地址要整体偏移,同时中断向量表要靠 VTOR 重映射。

/* APP 从 0x08020000 开始,给 Bootloader 留出 128KB */
MEMORY
{
    FLASH (rx) : ORIGIN = 0x08020000, LENGTH = 384K
    RAM   (rwx): ORIGIN = 0x20000000, LENGTH = 128K
}

代码里配套设置:

#define APP_VECTOR_OFFSET   0x20000

// 在 SystemInit 之后、main 之前完成重映射
SCB->VTOR = FLASH_BASE | APP_VECTOR_OFFSET;

只改链接地址不改 VTOR,中断会跳到 Bootloader 的向量表;只改 VTOR 不改链接地址,APP 的中断向量本身还在错误位置。两处必须同步。

复位后硬件固定从 0x00000000 取栈指针,所以 Bootloader 必须放在 Flash 起始,APP 的链接脚本偏移不能为 0。

如果你正在从零搭建裸机工程,或者被链接脚本里的 AT()、LMA/VMA 这些概念绕得头疼,不妨结合体系化的 C 语言与底层开发课程把启动流程和内存模型串起来看,很多“玄学问题”会变得有据可查。




上一篇:lwIP协议栈移植与TCP长连接通信实战:从PHY链路到自动重连
下一篇:Rust Axum Web 开发入门:路由、提取器、日志追踪与配置读取全流程
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-22 08:09 , Processed in 0.086623 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表