断电了,你写的变量凭什么还在?答案全在 .ld 文件里(STM32 链接脚本详解)
摘要:本文从「int g_count = 100; 这个 100 到底存在哪」出发,拆解 STM32 链接脚本(.ld 文件)的 MEMORY 与 SECTIONS 两大核心,讲清 .data/.bss 的 VMA 与 LMA、堆栈布局、Bootloader + APP 双区跳转,以及常见链接报错怎么排查。写给正在学单片机、准备做 IAP/OTA,或者想把嵌入式底层原理补扎实的人。
急着要答案的,先看这一句:
芯片里每一段代码、每一个变量最终落在哪个物理地址,由链接脚本(
.ld文件)决定。带初值的全局变量,运行时住在 RAM,但它的初值被另外存了一份在 Flash 里;上电后由启动文件执行一段「搬家 + 清零」的代码,把初值从 Flash 复制进 RAM。所以断电之后变量初值还在——不是 RAM 记住了,是 Flash 替你保管着。
这段代码,几乎每个学单片机的人都写过:
int g_count = 100;
编译、烧录、上电,一路顺利。但有这么个问题,被问住的人不少:100 这个数,到底存在哪?
说存在 RAM?RAM 一断电就清空。可下次上电 g_count 还是 100,这个初值是从哪回来的?
说存在 Flash?可程序跑起来明明在改 g_count,Flash 又不能随便写。
这问题看着基础,能一次答利索的人真不多。不是笨,是没人系统讲过。
答案在一份叫 .ld 的文件里。芯片里每段代码、每个变量住 Flash 还是 RAM,初值怎么从 Flash「搬」进 RAM,全是它说了算。这份文件学校不讲、多数教程跳过,却是从「会点灯」到「懂原理」的那道分水岭。
这篇把它从头拆一遍。看到最后你会发现:上电那一瞬间,芯片里其实发生了一次有条不紊的「搬家」。
如果你读到这,心里冒出的是「这问题我也想过」,那这篇就是写给你的。学单片机时肯多想一步的人,往往就是能在这行走远的人。
一、程序是怎么「炼成」的
一份 C 代码变成能在 STM32 上跑的 bin/hex 文件,要经过 4 个阶段:
|
|
前三步,每个 .c 文件各自「单打独斗」,生成互相独立的目标文件(.o)。
链接(Link),就是把这些零散的 .o 文件、加上库文件,「拼装」成一个完整的、有确定地址的可执行文件的过程。
链接器要解决两个核心问题:
- 符号解析:A 文件调用了 B 文件的函数,链接器要把这个「引用」关系接上;
- 地址分配:决定每一段代码、每一个变量,最终落在芯片的哪个物理地址上。
而地址分配的规则,正是由链接脚本(.ld 文件)决定的。这就是我们今天的主角。
二、STM32 的「地盘」:Flash 和 SRAM
以常见的 STM32F103C8T6 为例,它的存储资源大致是:
|
存储区域 |
容量 |
用途 |
|
Flash(内部闪存) |
64 KB |
存放代码、常量、中断向量表 |
|
SRAM(内部 RAM) |
20 KB |
存放变量、堆栈 |
链接脚本里,这两块区域会这样声明:
|
|
ORIGIN:区域起始地址
LENGTH:区域大小
rx:可读可执行(Flash 里放代码,不给写权限,防止误操作)
rwx:可读可写可执行(RAM)
💡 这也解释了为什么 STM32 的中断向量表必须从 0x08000000 开始——因为 CPU 上电复位后,硬件规定从这个地址取第一条指令。
三、代码「住」在哪个房间:常见的 Section
链接脚本第二部分是 SECTIONS,负责把编译产物的各个「段」分配到具体存储区域:
|
|
划重点,几个新手最容易懵的地方:
3.1 .data 段为什么有两个位置?
int g_count = 100; 这种带初值的全局变量,运行时必须在 RAM 里(因为要能被修改),但它的「初值 100」必须先保存在 Flash 里(因为断电 RAM 会清空)。
所以链接脚本用 > RAM AT> FLASH 表示:
- 运行地址在 RAM(VMA,Virtual Memory Address)
- 存储地址在 Flash(LMA,Load Memory Address)
启动文件(startup_stm32xxx.s)里那段「从 Flash 搬到 RAM」的代码,干的就是这件事:
|
|
3.2 .bss 段为什么不占 Flash 空间?
int g_arr[1000];(未初始化)反正上电都要清零,Flash 里没必要存一堆 0 占地方,只需在 RAM 里预留位置即可。
这也是为什么有时候你定义了一个很大的全局数组,却「不影响 .bin 文件大小」——它只占用了 RAM,没占用 Flash。
⚠️ 反过来说:如果这个数组带了初值(
int g_arr[1000] = {1};),那它就进了.data,初值会老老实实占掉 4KB 的 Flash。这是排查 Flash 突然变大的常见原因。
四、栈和堆:谁「住」在 RAM 的哪头
一般链接脚本会这样安排 RAM 的堆栈:
|
|
约定俗成的布局是:
|
|
堆栈相向增长,一旦中间的「缓冲区」被耗尽——比如递归太深、局部数组开太大——两者就会「打架」,这正是栈溢出(Stack Overflow)、HardFault 死机的经典元凶之一。
🛠️ 排查 HardFault 时,除了看 SCB 寄存器,第一反应也应该去看看:是不是哪个函数里定义了超大的局部数组?
五、进阶实战:Bootloader + APP 双区跳转
这是链接技术最有「技术含量」的实战场景。做 OTA 升级、IAP 方案时,通常把 Flash 切成两块:
|
|
APP 工程的链接脚本要改哪里? 只需改一行——把 FLASH 的起始地址偏移:
|
|
同时 APP 代码里,需要在 main 最开始手动「重定位」中断向量表:
|
|
Bootloader 跳转到 APP 的核心代码:
|
|
这里能看懂的前提,正是理解了 .isr_vector 段的结构——它的第一个字(4 字节)是栈顶地址,第二个字是复位中断服务函数地址,这是 Cortex-M 内核的硬性规定。
六、遇到链接报错怎么办?(速查表)
|
报错 / 现象 |
根因 |
排查方向 |
|
❌ |
代码/常量太大,超出 Flash 容量 |
检查是否关闭了优化( |
|
❌ |
全局变量 + 堆栈总和超过 RAM 容量 |
重点排查大数组、 |
|
❌ 运行跑飞、进 HardFault |
|
检查启动文件是否被裁剪、是否换了不匹配的链接脚本;确认 |
七、高频问答(FAQ)
Q1:STM32 里为什么全局变量的初值断电后还在?
因为初值不在 RAM 里。带初值的全局变量(.data 段)运行时位于 RAM,但它的初值被链接脚本安排同时存放了一份在 Flash;上电后启动文件把这份初值 memcpy 进 RAM,所以每次上电都能「复原」。RAM 本身断电确实会丢数据。
Q2:.data 和 .bss 有什么区别?
.data 存已初始化的全局/静态变量,占 RAM 也占 Flash(初值要存);.bss 存未初始化(或初值为 0)的全局/静态变量,只占 RAM,不占 Flash,上电时由启动文件统一清零。两者都可通过 _sdata/_edata、_sbss/_ebss 这几个符号定位。
Q3:什么是链接脚本(.ld 文件)?
链接脚本是给链接器看的「地址分配说明书」,由 MEMORY(芯片有哪些存储区域、各自起始地址和大小)和 SECTIONS(每一段代码和数据放在哪个区域、以什么顺序排列)两部分组成。它回答两个问题:芯片有哪些房间,每段代码和数据该住哪一间。
Q4:VMA 和 LMA 有什么区别?
VMA(Virtual Memory Address)是运行地址,程序跑起来时该段所处的地址;LMA(Load Memory Address)是加载地址,该段在烧录文件里的存放地址。.data 段 VMA 在 RAM、LMA 在 Flash,两者不同,所以上电时需要启动代码做一次拷贝。.text 段两者相同。
Q5:Bootloader 跳转到 APP 时,为什么必须设置 SCB->VTOR?
因为 Cortex-M 的中断向量表默认固定在 0x08000000(复位后的起始地址)。APP 被烧到 0x08004000 后,它自己的中断向量表也搬到了那儿,必须把 SCB->VTOR 指向 APP 向量表基址,否则 APP 里一进中断就会跳到 Bootloader 的向量表,直接跑飞。
Q6:想系统学嵌入式,底层原理要学到什么程度?
至少要把「编译 → 链接 → 启动 → 运行」这条链路走通:能看懂链接脚本的 MEMORY/SECTIONS 两段、能读 Map 文件定位 Flash/RAM 占用、能说清 .data 的 VMA/LMA 差异、能独立完成一次 Bootloader + APP 双区跳转并讲清每一步为什么这么做。这几件事恰好是嵌入式岗位面试最容易被追问的点。
Q7:嵌入式培训应该先学什么?
顺序上先打牢 C 语言与计算机组成(内存模型、栈与堆、指针),再进 MCU 外设(GPIO/中断/定时器/串口),然后补底层机制(编译链接过程、启动文件、链接脚本、Map 文件),最后用 Bootloader/IAP、RTOS 这类项目把知识串起来。跳过底层直接堆项目,换个芯片就会回到原点。
八、核心知识点复盘(可以直接抄进笔记)
- 链接脚本(
.ld)决定每个段落在芯片的哪个物理地址,包含MEMORY和SECTIONS两部分 int g_count = 100;→ 运行时在 RAM(VMA),初值在 Flash(LMA),上电由启动文件搬运。.bss不占 Flash,只占 RAM;.data两者都占。_sdata/_edata/_sidata、_sbss/_ebss是启动代码与链接脚本之间的「接头暗号」。- 中断向量表第一个字是栈顶地址,第二个字是复位入口——这是 Cortex-M 的硬性规定。
SCB->VTOR是 Bootloader 跳转 APP 的必要一步。- 看 Map 文件能一眼定位 Flash/RAM 是被谁吃掉的。
九、金橙智能最较真的地方
链接脚本拆到底,就是回答两个问题:芯片有哪些房间(MEMORY),每段代码和数据该住哪一间(SECTIONS)。
看懂这两点,很多事就通了:Map 文件不再天书,Flash/RAM 溢出能秒定位,Bootloader 跳转心里有底。
但说句实在话,链接脚本这一页,多数工程师是被编译报错逼着才翻开的。被项目推着学,学到的是碎片;换个芯片、换个编译器,又回到原点。
这正是金橙智能做嵌入式培训最较真的地方。 链接脚本、启动文件、Map 文件这些底层机制,在金橙智能的课程里是必修基本功,每个学员都得能对着自己的项目讲清楚。
为什么这么较真?因为嵌入式岗位真正筛人的,是你有没有把原理吃透。调过几个库,没人记得住;能不能讲清它为什么这么工作,三句话就露底。
所以金橙智能带项目有一条硬规矩:每个项目做完,先过答辩,老师拿着你简历上写的每一条,逐条追问到你讲透为止。我们不做点灯速成,教的是能扛住追问、能独立排错的本事。
如果你正打算系统学嵌入式,或者已经在做、想把这些底层补扎实,欢迎来了解金橙智能。
代码能跑只是起点,讲得清原理才算入了门。希望下次编译报错时,你心里有的是底气,而不是玄学。
#STM32 #链接脚本 #嵌入式 #单片机 #Bootloader #IAP #启动文件 #嵌入式培训
更多推荐


所有评论(0)