彻底搞懂 LMA 与 VMA:GNU LD 链接脚本与 ARMCC 分散加载深度对比
在嵌入式开发领域,将代码和数据从非易失性存储(Flash)“搬运”到高速缓存(RAM)运行是家常便饭。对于习惯了 ARMCC(Keil MDK)的开发者来说,Scatter File(分散加载文件)像是一个黑盒,一切都能自动完成;而转到 GNU LD(GCC)后,往往会被 Linker Script 那晦涩的语法折磨。
本文将深入探讨这两者在处理 LMA(Load Memory Address,加载域地址) 与 VMA(Virtual Memory Address,运行域地址) 上的本质区别,帮助你理清底层重定位的逻辑。
1. 核心概念:LMA vs VMA
在嵌入式系统中,程序镜像存储在 Flash 中,但其中的 .data 段(已初始化的全局变量)和一些对速度要求极高的代码段(如中断向量表或某些算法)必须搬移到 RAM 中运行。
- LMA (Load Memory Address):程序物理存储的位置(通常在 Flash 内)。
- VMA (Virtual/Execution Memory Address):程序实际运行时的位置(通常在 RAM 内)。
链接器的核心任务,就是告诉程序:“你现在存在 A 处,但你运行的时候得认为自己在 B 处。”
2. ARMCC (Keil):声明式的“全自动”体验
在 ARMCC 的 .sct 文件中,LMA 和 VMA 的关系是通过嵌套结构定义的。
语法示例:
LR_IROM1 0x08000000 0x00080000 { ; Load Region (LMA)
ER_IROM1 0x08000000 0x00080000 { ; Execution Region 1 (Flash 运行)
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00020000 { ; Execution Region 2 (RAM 运行)
.ANY (+RW +ZI)
}
}
关键特征:
- 分层结构:
LR_xxx(Load Region)定义了 LMA 的起始位置;内部的ER_xxx(Execution Region)定义了 VMA。 - 符号自动生成:ARMCC 链接器会自动生成类似
Image$$RW_IRAM1$$Base、Image$$RW_IRAM1$$Length等符号。 - 库函数接管:ARMCC 最“神奇”的地方在于,你不需要手动写代码从 Flash 拷贝
.data段到 RAM。在进入main函数之前,C 库中的__main函数(非main)会读取链接器生成的 Region Table,自动完成数据的搬移(Copy)和 ZI 段的清零(Zero-init)。
3. GNU LD (GCC):指令式的“纯手工”逻辑
GNU LD 的 .ld 文件更加底层,它不关心你的库函数是否强大,它只负责建立映射关系。
语法示例:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.text : {
*(.text*)
*(.rodata*)
} > FLASH
/* 注意这里的 AT 关键字 */
.data : {
_sdata = .; /* VMA 起始 */
*(.data*)
_edata = .; /* VMA 结束 */
} > RAM AT > FLASH /* 运行在 RAM,存储在 FLASH */
_sidata = LOADADDR(.data); /* 获取 .data 段在 Flash 中的 LMA */
}
关键特征:
- AT 关键字:这是定义 LMA 的关键。
> RAM指定了 VMA,而AT > FLASH(或AT(addr)) 强制指定了该段的物理加载地址。 - 符号手动定义:你需要显式地定义
_sdata、_edata和_sidata(通常使用LOADADDR函数获取)。 - 手动搬运:GCC 编译器本身不会帮你生成搬运逻辑。你必须在
startup_xxx.s或者SystemInit之后的 C 代码中,通过一段memcpy风格的代码,将数据从_sidata拷贝到_sdata之间。
4. 深度对比:两者最大的鸿沟在哪里?
| 特性 | ARMCC (Scatter File) | GNU LD (Linker Script) |
|---|---|---|
| 思维模型 | 容器模型(大盒子套小盒子) | 流式映射(从输入段到输出段) |
| LMA 指定方式 | 定义在父级 Load Region |
使用 AT 或 AT> 指令 |
| 数据搬运 | 自动化。由 __main 依靠 Region Table 完成 |
手动化。由开发者在 Startup 逻辑中实现 |
| 灵活性 | 较低。受制于 ARMLib 的启动流程 | 极高。可以精准控制任意 Section 的位置 |
| 符号引用 | 自动生成 Image$$... 格式 |
需手动在脚本中使用 . 或 PROVIDE 定义 |
5. 实战避坑指南
坑一:LMA 地址对齐
在 GNU LD 中,如果你对 .data 段设置了 VMA 对齐,一定要注意 LMA 也会随之产生空隙。如果搬运代码没有处理好 LMA 的偏移,会导致数据错位。建议使用 ALIGN 时同时显式对齐 VMA 和 LMA。
坑二:ARMCC 的“隐藏”压缩
ARMCC 为了节省 Flash 空间,有时会对 RW 段进行压缩(RW Data Compression)。如果你在某些裸机启动代码中绕过了 __main 直接调 main,不仅数据没搬过去,甚至解压逻辑都没触发,会导致程序运行瞬间崩溃。
坑三:VMA 确定与调试
在调试时,查看生成的 .map 文件。
- ARMCC Map:关注
Execution Region的 Base 地址。 - GCC Map:关注
VMA和LMA两列是否一致。如果不一致,检查startup.s里的搬运逻辑是否覆盖了该段。
总结
ARMCC 的 Scatter File 像是一个智能管家,它通过高度封装简化了嵌入式开发的门槛;而 GNU LD 像是一套精密的手动挡工具,它把内存布局的所有权完全交给了开发者。
理解 LMA 与 VMA 的差异,本质上是理解程序从存储状态到运行状态的转换过程。无论使用哪种工具,核心都在于确保运行时刻 RAM 里的数据与链接器预设的 VMA 完全吻合。