☰
嵌入式进阶——你的变量住在哪里?
2026/10/27 10:33:20 网站建设 项目流程

MCU拆解室——你的变量住在哪里?

配套视频:p02_memory_map_1080p30.mp4(1920×1080@30,7 分 22 秒,8 章)【MCU拆解室–你写的变量到底在哪儿-哔哩哔哩】 https://b23.tv/BV13TJe6NEDt【MCU拆解室–你写的变量到底在哪儿】
参考芯片:STM32L552(Cortex-M33,512 KB Flash / 256 KB SRAM)
注意:系列从下一期(编译与链接)起改用经典 STM32F103(64 KB / 20 KB)。概念完全一致,只是数字不同;阅读时把 512/256 换成 64/20 即可。
本文定位:对照阅读版。视频里出现的每个数字(12340 字节、七个变量、1540 字节堆栈)都来自真实工程;本文补上视频受时长限制没展开的推导与调试方法。


阅读指南

章节对应视频时间码一句话
0——三个问题
1L1 开场提问00:00一盏灯动了三块地皮
2L2 两块地盘00:56Flash / SRAM / 外设,以及段
3L3 编译流水线01:46段是在声明时定下的
4L4 变量安置02:26七个变量的户口
5L5 生命周期×作用域03:22活多久、谁能看见
6L6 上电那一刻04:20搬 .data、清 .bss、跳 main
7L7 三关键字05:17const / static / volatile
8L8 总结06:26map 文件与两个恒等式

第 0 章 三个问题

  1. const uint8_t SEQ[] = {...}放在哪?为什么不占 RAM?
  2. volatile uint32_t g_ms;为什么必须 volatile?不加会怎样?
  3. int seq_copy[4];在函数里,它什么时候出生、什么时候消失?

一句话立意:变量进哪个段,看声明;活多久、谁能碰,也看声明。

图 0-1 点灯的三句话动了三种资源(视频 00:20)


第 1 章 一盏灯,三块地皮

视频用点灯代码引出内存话题。一段最普通的点灯循环,其实同时用了三个地方:

constuint8_tSEQ[]={1,0,0,0};// ① 只读常量 → Flashfor(;;){GPIOA->BSRR=SEQ[i]<<5;// ② 写外设寄存器 → 外设空间for(volatileuint32_tg=0;g<200000;g++);// ③ 循环变量 → 栈/寄存器}
句子资源特性
读SEQFlash(只读)掉电不丢,读不花 RAM
写BSRR外设寄存器空间不经过内存,直接到 GPIO
循环变量栈 / 寄存器掉电即失,函数返回即消失

编译器给的账单(真实工程):Flash 用了 12340 字节,RAM 1680 字节。这两个数字,就是"地图"的入口。

为什么先讲这个?因为初学者常以为"程序 = 代码 + 变量",其实中间还有一层:变量被分门别类放进不同的段,段再被链接脚本放到不同的地址。不理解这一层,HardFault、RAM 溢出、volatile 失效都只能靠猜。


第 2 章 三块地盘与七个段

图 2-1 芯片版图:左边内核,右上 Flash,右下 SRAM,右下角外设(视频 01:15)

2.1 三块地盘

地盘起始地址本集容量掉电放什么
Flash0x0800_0000512 KB不丢向量表、代码、只读常量、.data初值模板
SRAM0x2000_0000256 KB丢.data运行副本、.bss、堆、栈
外设0x4000_0000——GPIO/USART/SPI 等寄存器

地址就是门牌号:CPU 不区分"内存"和"外设",它只往地址上读写;是总线译码器把地址分发到对应设备。

2.2 Flash 里的段

段内容说明
.isr_vector向量表复位后 SP/PC 从这里读
.text函数代码只读、可执行
.rodataconst常量、字符串只读
.data(LMA)有初值变量的初值模板运行时会被搬到 SRAM

2.3 SRAM 里的段

段内容何时初始化
.data(VMA)有初值的全局/静态变量启动时从 Flash 拷贝
.bss初值为 0 的全局/静态变量启动时整片清零
堆malloc用运行时向上长
栈局部变量、调用现场运行时向下长

关键认识:.data在 Flash 和 SRAM 各有一份(模板 + 运行副本),这正是第 6 章"上电为什么要搬"的原因。


第 3 章 段是在"声明"时定下的

图 3-1 .c → .o → 链接:段在编译期确定,地址在链接期确定(视频 02:35)

  • 编译阶段:每个.o里已经有.text/.rodata/.data/.bss这些小段,但地址是相对/未定的。
  • 链接阶段:链接脚本把同名的段合并,分配最终地址(VMA),.data还会另给一个装载地址(LMA)。

一句话:变量落在哪个段,在你写下声明的那一刻就定了;具体地址,链接器说了算。

这也是为什么"同样的代码,换个链接脚本,地址就全变了"。调试时看到的0x2000_0060,是链接脚本 + 当前编译结果的产物,不是语言规定。


第 4 章 七个变量的户口

视频里逐个安置了七个变量。它们的声明(节选):

constuint8_tSEQ[4]={1,0,0,0};// ① 只读查表volatileuint32_tg_ms;// ② 中断里减、主循环读uint32_tg_err_cnt;// ③ 全局计数staticuint8_ts_seq_idx;// ④ 文件级 staticstaticuint8_ts_ps;// ⑤ 函数级 static(在 SysTick 里)voidf(void){intseq_copy[4];...}// ⑥ 局部数组voidg(void){volatileuint32_t*bsrr;}// ⑦ 局部指针
#变量段寿命可见范围
①SEQ.rodata(Flash)全程本文件(const+static语义)
②g_ms.data(有初值)/.bss(初值 0)全程全工程
③g_err_cnt.bss全程全工程
④s_seq_idx.bss全程本文件
⑤s_ps.bss全程本函数
⑥seq_copy[4]栈仅函数调用期本代码块
⑦bsrr(指针本身)栈仅函数调用期本代码块

注意三个细节:

  1. static不改变位置,只收窄可见范围(①④⑤ 都在"全程"那一列)。
  2. 显式写= 0也进.bss,不进.data——编译器知道 0 不用存。
  3. 局部数组在栈上:编译器只决定"去栈上",具体地址运行时由 sp 决定。

4.1 为什么g_ms必须 volatile

g_ms被两个执行流共享:SysTick 中断里减,主循环里等它到 0。不开volatile时,编译器会把g_ms缓存进寄存器(它"看不到"中断会改它),主循环就再也读不到新值。

这是第 7 章的主题,也是下一期 GPIO 的伏笔。


第 5 章 生命周期 × 作用域

图 5-1 左边代码、右边四个同心圈:代码块 / 函数 / 文件 / 全工程(视频 03:55)

5.1 两个维度

  • 寿命(lifetime):变量从什么时候存在到什么时候消失。
  • 作用域(scope):代码的哪些位置能"看见"这个名字。

两者不是一回事。典型例子:

voidf(void){staticints=0;// 寿命:全程;作用域:仅 f()inta=1;// 寿命:函数调用期;作用域:本代码块}

5.2 四层可见范围

层谁能看见例子
代码块{}块内for里的i
函数函数内形参、局部变量
文件本.c内加static的全局变量
全工程所有.o不加static的全局变量(外部链接)

static的两个含义要分清:修饰全局变量/函数时是"收窄可见范围到本文件";修饰局部变量时是"把寿命拉长到全程"。同一个关键字,两个方向。

5.3 栈变量的"生死"

seq_copy[4]在进入函数时随sub sp出现,返回时随pop消失(第 5 章的栈帧)。返回后它的地址就失效了——这就是"不能返回局部变量地址"的原因。


第 6 章 上电那一刻:谁把变量放到位置上

图 6-1 启动代码:搬 .data、清 .bss、跳 main(视频 04:45)

复位后的顺序(真实启动文件):

  1. 硬件:从[0x0000_0000]读栈顶 → 装 SP;从[0x0000_0004]读复位向量 → 装 PC。
  2. Reset_Handler:调SystemInit(配时钟)。
  3. 搬.data:从 Flash 的 LMA(_sidata)拷到 SRAM 的 VMA(_sdata…_edata)。
  4. 清.bss:_sbss…_ebss整片写 0。
  5. bl main:PC 跳到main。

为什么.data要搬?因为初值必须"保留"(Flash 不丢),而运行时要"能改"(必须可写 RAM)。Flash 不可写,所以只能复制一份到 SRAM。

这一段和 P04《启动》是同一件事,那期会逐行拆启动文件;这里只要记住"三件事:搬、清、跳"。


第 7 章 三个关键字

图 7-1-O2下两版汇编:无 volatile 只读一次内存,有 volatile 每圈重读(视频 05:50)

7.1 const

  • 全局const→.rodata(Flash),省 RAM;
  • 但局部const不一定进.rodata:它可能仍在栈上,只是"你不能改"。

7.2 static

  • 全局加static:外部文件看不见(避免命名冲突);
  • 函数内加static:只初始化一次,寿命全程;
  • 静态局部变量的初始化发生在"第一次执行到声明处之前"(C 标准里是程序启动时,实现上由启动代码清零 + 首次进入时赋值),不是每次调用都初始化。

7.3 volatile

while(g_ms){}// 忙等
编译无 volatile有 volatile
-O0每圈都ldr每圈都ldr
-O2只读一次,cbz后死循环每圈ldr+cbz

边界(重要):

  • volatile只保证"每次都真的访问内存",不保证原子;
  • 不是内存屏障(编译器和 CPU 都可能重排其他访问);
  • 读-改-写(g_ms++)在中断并发下仍需临界区/原子指令。

这一条是嵌入式面试高频题:volatile解决"看不见的修改",不解决"竞争"。


第 8 章 总结:map 文件与两个恒等式

图 8-1 map 文件、两个恒等式、三个翻车现场(视频 06:50)

两个恒等式:

Flash 占用 = .isr_vector + .text + .rodata + .data(LMA) RAM 占用 = .data(VMA) + .bss + 堆 + 栈

三个真实翻车现场:

现象原因定位
HardFault上电即死局部大数组撑爆栈看 SP 与_estack距离
region RAM overflowed.bss太大看 map 文件
while(g_ms==0);卡死没加volatile看-O2反汇编

五句话:

  1. Flash 存不变的(代码、常量、初值模板)。
  2. RAM 分四块(.data、.bss、堆、栈)。
  3. 变量进哪段看声明:const→Flash,初值 0→.bss,局部→栈。
  4. static管"谁能看见"(全局)或"活多久"(局部)。
  5. volatile管"每次都真读",不管原子、不管顺序。

自测(8 题)

  1. 为什么.data在 Flash 和 SRAM 各有一份?
  2. 显式写= 0的全局变量进哪个段?
  3. 函数内static int s;的寿命和作用域?
  4. 局部的const int a = 5;一定在 Flash 吗?
  5. .bss不占 Flash,为什么?
  6. 不加volatile,-O2下忙等为什么可能死循环?
  7. volatile能保证g_ms++原子吗?
  8. 栈和堆在链接脚本里没有显式分配时,会发生什么?
参考答案
  1. Flash 存初值模板(掉电不丢),SRAM 存运行副本(可写)。
  2. .bss(编译器知道 0 不用存)。
  3. 寿命全程;作用域仅该函数。
  4. 不一定;局部const常在栈上,只是不可改。
  5. .bss全 0,启动时整片清零即可,不需要在 Flash 存 0。
  6. 编译器把g_ms缓存进寄存器,循环条件永远用旧值。
  7. 不能;需要关中断/原子指令。
  8. 栈堆共享剩余 RAM,向中间生长,撞上就 HardFault 或踩坏数据。

附录 本期关键数字

项值
Flash 占用12340 B
RAM 占用1680 B
栈/堆共享剩余1540 B(后续 F103 工程实测)
七个变量SEQ / g_ms / g_err_cnt / s_seq_idx / s_ps / seq_copy / bsrr
段.isr_vector/.text/.rodata/.data/.bss/ heap / stack

成片与本文数值均来自同工程;换芯片/换链接脚本后数字会变,方法不变。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询