4、程序分段与地址映射:代码段、数据段、BSS段在Flash和RAM中的分布、如何通过链接脚本控制地址。
好,咱们接着聊。上一章我们把启动流程捋了一遍,这一章要深入一个非常核心的话题——程序分段与地址映射。
说白了,就是你的代码编译完之后,那些.text、.data、.bss段到底该放哪儿?谁在Flash里,谁在RAM里?怎么通过链接脚本精确控制它们的地址?
我个人习惯把链接脚本比作「一张地图」。没有这张地图,你的固件就像无头苍蝇,跑飞了都不知道。
4.1 程序段的本质:为什么需要分段?
你想想看,MCU的存储空间是分层的。Flash是非易失的,掉电不丢,但速度慢;RAM是易失的,速度快,但掉电就没了。
所以,一个聪明的做法是:
- 代码段(.text):只读,放Flash里,省RAM。
- 只读数据段(.rodata):常量、字符串,也放Flash里。
- 数据段(.data):有初始值的全局变量。运行时必须在RAM里,但初始值得从Flash拷过去。
- BSS段(.bss):未初始化的全局变量。运行时在RAM里,启动时清零。
嗯,这里要注意:.data段在Flash里存的是「初始值的镜像」,上电后由启动代码拷贝到RAM里。这个拷贝过程,就是我们上一章说的__main或者Reset_Handler里干的事。
核心概念:程序在Flash中是「静态分布」,在RAM中是「运行时分布」。链接脚本就是定义这个映射关系的规则书。
4.2 典型的内存分布图
我画一个典型的ARM Cortex-M的分布图,你感受一下:
Flash (0x08000000 起始)
+------------------+
| .text (代码) | <- 入口地址
| .rodata (常量) |
| .data (初始值镜像) | <- 启动时拷贝到RAM
+------------------+
RAM (0x20000000 起始)
+------------------+
| .data (运行时) | <- 从Flash拷贝过来
| .bss (未初始化) | <- 启动时清零
| heap (堆) |
| stack (栈) |
+------------------+
为什么会这样?因为CPU只能从Flash取指令,但读写变量必须用RAM。所以代码段老老实实呆在Flash里,而数据段必须「搬家」到RAM里。
我在项目中遇到过一个问题:有个同事把一个大数组定义成const,以为能省RAM。结果编译器把它放到了.rodata段,还是在Flash里。读取速度慢不说,还占用了宝贵的Flash空间。嗯,这就是对段分布理解不够深。
4.3 链接脚本:地址的「总设计师」
链接脚本(.ld文件)就是干这个的。它告诉链接器:每个段放在哪个地址,按什么顺序排列。
我以STM32的GCC链接脚本为例,给你拆解一下:
/* 定义内存区域 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
/* 定义段布局 */
SECTIONS
{
/* 代码段放在Flash起始 */
.text :
{
*(.isr_vector) /* 中断向量表 */
*(.text) /* 代码 */
*(.rodata) /* 只读数据 */
_etext = .; /* 标记代码段结束 */
} > FLASH
/* 数据段:初始值镜像放在Flash */
.data : AT (_etext)
{
_sdata = .; /* RAM中数据段起始 */
*(.data) /* 数据 */
_edata = .; /* RAM中数据段结束 */
} > RAM AT> FLASH
/* BSS段:直接放在RAM */
.bss :
{
_sbss = .; /* BSS起始 */
*(.bss) /* 未初始化数据 */
_ebss = .; /* BSS结束 */
} > RAM
}
注意看.data : AT (_etext)这一行。它表示:.data段运行时在RAM里,但它的加载地址(LMA)在_etext之后,也就是Flash里。启动代码就是根据_sdata、_edata和_etext这三个符号来完成拷贝的。
避坑指南:我曾经在移植一个Bootloader时,忘记在链接脚本里定义_sdata和_etext符号,结果启动代码找不到数据段的源地址,全局变量全是乱的。排查了整整一天才发现是链接脚本少写了两个符号定义。
4.4 自定义段:Bootloader与APP的「隔离墙」
在Bootloader和APP跳转的场景下,链接脚本的地址控制就更加关键了。
比如,Bootloader放在Flash的0x08000000~0x0800FFFF,APP放在0x08010000之后。那么APP的链接脚本就要这样写:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08010000, LENGTH = 448K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.text :
{
*(.isr_vector) /* 中断向量表偏移到0x08010000 */
*(.text)
*(.rodata)
_etext = .;
} > FLASH
/* 其他段不变 */
...
}
同时,APP的中断向量表偏移也要设置:
SCB->VTOR = 0x08010000;
嗯,这里要注意:如果你不改VTOR,中断来了还是会跳到Bootloader的向量表,APP就永远跑不起来。
警告:链接脚本中的ORIGIN和LENGTH必须与硬件实际地址一致。我曾经见过有人把Flash的ORIGIN写成了0x08000000,但APP实际烧录在0x08010000,结果一运行就HardFault。这种错误非常隐蔽,因为编译不报错,只有运行才崩溃。
4.5 实战技巧:如何验证地址映射是否正确?
我建议你每次写完链接脚本,都做两件事:
- 查看map文件:编译生成的
.map文件里,有每个段的起始地址和大小。比如:.text 0x08010000 0x4000,说明代码段从0x08010000开始,大小16KB。 - 用调试器检查:在启动代码里打断点,查看
_sdata、_etext、_sbss、_ebss的值是否与预期一致。
我记得有一次,我写了一个很复杂的链接脚本,把部分代码放到了RAM里(为了加速)。结果map文件显示.text段在Flash和RAM里都有。我一开始以为错了,后来才明白这是「代码重定位」——把关键函数拷贝到RAM里执行。嗯,这个技巧后面章节会细讲。
4.6 总结一下
程序分段和地址映射,说白了就是「什么数据该放哪里」。链接脚本就是你的控制工具。
- 代码段:放Flash,只读。
- 数据段:初始值在Flash,运行时在RAM。
- BSS段:运行时在RAM,启动时清零。
- 链接脚本:通过
MEMORY和SECTIONS精确控制每个段的地址。
下一章,我们会讲「跳转前的状态清理」,看看在跳转之前,哪些寄存器必须恢复,哪些可以不管。敬请期待。