VMA和LMA
一、预备知识:
在深入代码之前,我们先对齐几个在跨模块排查时经常遇到的核心概念:
LMA (Load Memory Address)(0x80000000):烧录地址。MCU 掉电后数据持久存储的位置(通常是 Flash)。
VMA (Virtual Memory Address)(0x20000000):运行地址。程序跑起来后,CPU 去哪里找这个变量(通常是 RAM)。
对于不需要在 RAM 运行的代码或常量,LMA 和 VMA 是一样的。
由于cpu执行的地址都是虚拟地址,经过MMU转为物理地址。在没有开MMU的裸板下,延续了这一称呼。
Section (段):编译器将相同性质的数据归组打包的内存分组单元(如 .bss、.data),是链接脚本分配内存的基本单位。
因此,当编译地址(加载地址)和运行地址相同时,绝对跳转和相对跳转都可以正确执行。
但是,当编译地址(加载地址)和运行地址不相同时,相对跳转就会出现问题。
二、示例:LDR 寻址溢出与编译器优化的冲突
1. 现象描述
在实际工程中,若代码段(.text)与数据段(.rodata)在物理内存中相隔较远(例如相隔 128KB),在开启 -O2 高阶优化时极易引发死机(HardFault)。而将优化等级降为 -O0 时,程序却能正常运行。
2. 底层原理剖析
指令寻址范围的差异:
BL(Branch with Link):用于函数跳转,拥有 24位 偏移量,寻址范围高达 ±16MB。因此,跨越 128KB 的函数调用完全不会有问题。LDR R0, [PC, #offset]:用于加载局部常量或文字池数据,仅有 12位 偏移量,寻址范围被死死限制在 ±4KB 以内。
为什么
-O0没事,-O2却死机?在
-O0下,代码线性排列,编译器生成的“文字池(Literal Pool)”通常紧挨着函数本体,距离数据段较远时,编译器可能会采取其他保守策略,或者碰巧未触发长距离访问。在
-O2下,编译器会进行激进的内联、循环展开和代码重排。这会导致函数体积膨胀或位置移动。当函数内部需要读取 128KB 外的全局常量时,编译器仍会默认生成高效的LDR [PC, #offset]指令。由于距离远超 ±4KB,偏移量发生溢出(截断),CPU 最终读取到了 PC 附近的错误指令(被误当成数据),从而引发死机。

第一个0x08060000是运行地址(VMA),AT中的是烧录地址(LMA)
如果按下图写,将会出现BUG

从这张图片我们可以看到,这里没有指定VMA和LMA,这导致编译器在处理这段的代码时,会LDR偏移量不够,无法生成有效的机器码。
正常情况:
LDR R0, =my_constant @ 编译器生成:LDR R0, [PC, #offset]在未显式配置 VMA(运行地址)和 LMA(烧录地址)时,反汇编显示使用了 LDR 指令进行取值。由于 LDR 的寻址范围受限,导致程序根本无法跳转至目标地址执行。通过修改链接脚本,强制设置 VMA = LMA。修改后,链接器会生成 BL(Branch with Link)跳转指令。由于 BL 指令的寻址范围远大于 LDR,成功解决了跨段跳转失败的问题,程序得以正常启动。
如果不想修改链接脚本,可以采用内联汇编代码(见替代办法):
替代办法:
MOVW R0, #:lower16:my_constant @ 加载低16位
MOVT R0, #:upper16:my_constant @ 加载高16位
LDR R0, [R0] @ 间接访存但代价是:代码体积膨胀,原来 1 条 LDR(4字节)变成 3 条指令,多执行 2 条指令,额外消耗 2 个时钟周期,多占一个通用寄存器存放地址。
三、启动代码的搬家工作:
.data : /*数据段 已初始化的变量*/
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
*(.RamFunc)
*(.RamFunc*)
. = ALIGN(4);
_edata = .;
} >RAM AT> APP_A /*AT就是指定加载地址也就是FLASH,真正要读写变量,要去RAM里找*/我们都知道,DATA段是可读可写的,这就说明这一部分的代码要加载到RAM中,并且是有初值的,但是究竟是谁在搬运这一部分的变量到RAM中呢,就是启动文件!
在你按下开发板复位键、CPU 开始执行代码,但还没进入你的 main() 函数之前,启动代码会在后台默默完成以下动作:
启动代码去 Flash 的 LMA 地址,找到
.data段变量的初始值。启动代码去 RAM 的 VMA 地址,找到分配给这些变量的内存空间。
启动代码写了一个死循环,把 Flash 里的初始值,一个字节一个字节地拷贝到 RAM 中。
对于
.bss段,启动代码会把 RAM 中对应的工位全部清零。
四、bin文件中很奇怪的现象:
作者在编写这个项目时,是采用多程序分块编译出Bin文件后,计算相应偏移地址后拼接出一个大的bin文件,实际上这种情况只能用来做烧录使用,但是在拼接过程中,发现了以下这种现象。
背景:当 APP_A 结束在
0x08040000,而下一个要用的数据段被强制指定在0x08050000时,中间就空出了 64KB 的空间。坑点:
objcopy工具在把.elf转换成.bin文件时,默认会压缩文件。它看到中间有空洞,就会直接把后面的数据挪过来,导致.bin文件变小了。结果:这就会导致,实际用二进制查看bin文件时,发现根本无法拼出应有大小的bin文件
objcopy 的默认行为是连续排列数据,它不会在 .bin 中插入 0x08040000 ~ 0x0804FFFF 的 64KB 空洞。所以烧录后,.data 的初始值实际在 Flash 的 0x08020000 + sizeof(.text) 位置,而不是 0x08050000。
解法:
. = ALIGN(0x10000);
BYTE(0xFF)这种方式确实可以强制 objcopy 在文件中插入填充字节,让 .bin 的大小膨胀到正确的偏移量。但代价是 .bin 文件变大,且 0xFF 会浪费 Flash 空间。
更好的做法是用 objcopy --gap-fill=0xFF --pad-to=0x08050000,这样不需要修改链接脚本,直接在命令行指定填充:
arm-none-eabi-objcopy -O binary --gap-fill=0xFF output.elf output.bin受限于个人水平,文中难免有理解不到位或笔误之处。恳请各位大佬不吝赐教,欢迎邮件交流:sharkey_z@163.com
