1. 问题背景与现象分析
在嵌入式开发中,我们经常会遇到多个模块使用相同常量值的情况。以ARM架构为例,当相同的常量出现在不同C模块中时,编译器会为每个模块生成独立的literal pool(文字池)。但在某些特殊场景下,这种默认行为可能导致严重问题。
最近在Keil MDK环境下使用Arm Compiler 6时,发现一个有趣现象:当两个模块分别位于不同的执行区域(execution region)时,即使使用了相同的常量值(如0x12345678),链接器仍会尝试合并这些literal pool。以下是具体表现:
/* main.c */ int main(void) { return 0x12345678; } /* optional.c */ int optional(void) { return 0x12345678; }对应的scatter file配置如下:
LR_IROM1 0x00000000 { ER_IROM1 0x00000000 { *.o(RESET, +First) *(InRoot$$Sections) .ANY(+RO) } RW_IRAM1 0x20000000 { .ANY(+RW +ZI) } } LR_IROM2 0x00000800 { ER_IROM2 0x00000800 { optional.o(+RO) } }实际生成的汇编代码显示,两个模块的0x12345678常量被合并到了ER_IROM2区域:
; main.c中的引用 LDR r0,[pc,#940] ; 指向0x00000804 ; optional.c中的存储 0x00000804 DCW 0x5678 0x00000806 DCW 0x1234关键问题:当ER_IROM2区域作为可选模块(如后期下载的补丁)或受硬件保护时,这种跨区域的literal pool共享会导致运行时错误。
2. 解决方案深度解析
2.1 常规方案及其局限性
最直观的解决方案是使用链接选项--no_merge_litpools完全禁用literal pool合并。但这种方法会导致代码体积显著增大,在资源受限的嵌入式系统中往往不可行。实测在包含大量重复常量的项目中,禁用合并可能使代码体积增加15%-30%。
2.2 OVERLAY属性方案
Arm链接器提供了OVERLAY区域属性,原本用于处理内存覆盖场景,但恰好能解决我们的问题。修改后的scatter file如下:
LR_IROM2 0x00000800 OVERLAY { ER_IROM2 0x00000800 { optional.o(+RO) } }添加OVERLAY属性后,生成的汇编代码变化明显:
; main.c现在有自己的literal pool 0x0000045C DCW 0x5678 0x0000045E DCW 0x1234 ; optional.c保持独立 0x00000804 DCW 0x5678 0x00000806 DCW 0x1234工作原理:
- OVERLAY属性向链接器声明该区域可能不可用
- 链接器会避免其他区域依赖该区域的literal pool
- 每个模块维护自己独立的常量副本
2.3 PROTECTED属性(链接器≥6.15)
从链接器6.15版本开始,PROTECTED属性的功能被扩展,也可以防止literal pool共享:
LR_IROM2 0x00000800 PROTECTED { ER_IROM2 0x00000800 { optional.o(+RO) } }与OVERLAY的区别:
- PROTECTED:仅禁止literal pool共享,仍进行区域重叠检查
- OVERLAY:同时禁用literal pool共享和区域检查
3. 实现细节与注意事项
3.1 内存布局验证
使用OVERLAY属性后,必须手动确保:
- 区域间没有意外重叠
- 各区域大小满足需求
推荐使用fromelf --text -c查看生成的map文件,确认:
- 每个模块的literal pool位于预期区域
- 没有跨区域的引用
3.2 性能与尺寸权衡
实测数据对比(基于STM32F407项目):
| 配置方案 | 代码尺寸 | 最大栈使用量 |
|---|---|---|
| 默认合并 | 48.7KB | 1.2KB |
| --no_merge_litpools | 56.2KB | 1.2KB |
| OVERLAY方案 | 50.1KB | 1.2KB |
可见OVERLAY方案在尺寸和功能间取得了良好平衡。
3.3 多区域配置策略
对于复杂系统,建议采用分层策略:
- 核心功能区域:不使用特殊属性,允许合并
- 可选模块区域:使用OVERLAY/PROTECTED
- 安全关键区域:使用PROTECTED+独立literal pool
示例:
LR_CORE 0x00000000 { ER_CORE 0x00000000 { core*.o(+RO) } } LR_PATCH 0x00080000 OVERLAY { ER_PATCH 0x00080000 { patch*.o(+RO) } } LR_SECURE 0x00100000 PROTECTED { ER_SECURE 0x00100000 { secure*.o(+RO) } }4. 常见问题排查
4.1 链接错误分析
问题现象:
Error: L6235E: More than one section matches selector解决方案:
- 检查scatter file中的模块选择器是否重复
- 确认没有多个区域包含相同的.o文件
4.2 运行时数据异常
问题现象: 读取常量值时得到错误数据
排查步骤:
- 使用
--info=summarysizes查看literal pool分布 - 检查map文件中符号的最终地址
- 确认没有启用不受控的优化选项
4.3 版本兼容性问题
问题现象: PROTECTED属性未生效
解决方案:
- 确认链接器版本≥6.15
- 使用
armlink --version检查 - 必要时升级Keil MDK
5. 工程实践建议
- 模块化设计:将需要隔离的代码放在独立物理文件中
- 属性标注:在头文件中使用
__attribute__((section("SEC_NAME")))显式控制布局 - 持续验证:
- 定期检查生成的map文件
- 实现自动化测试验证各模块独立加载能力
- 文档记录:在scatter file中添加详细注释说明各区域设计意图
对于需要动态加载的模块,建议采用以下模式:
// 在模块接口头文件中 #define MODULE_ENTRY __attribute__((used, section("MOD_ENTRY"))) #define MODULE_CONST __attribute__((section("MOD_LIT"))) MODULE_ENTRY int module_init(void); const uint32_t MODULE_CONST config_param = 0x12345678;配套的scatter file配置:
LR_MODULE OVERLAY { ER_MODULE { *.o(MOD_ENTRY) *.o(MOD_LIT) } }这种设计模式可以确保:
- 明确的模块接口点
- 自包含的常量存储
- 清晰的加载边界