在嵌入式系统开发中,链接器(Linker)扮演着至关重要的角色。不同于桌面应用的开发,嵌入式系统往往面临严格的内存限制和实时性要求。以Cortex-M系列微控制器为例,典型的片上Flash可能只有256KB,SRAM仅64KB。在这种资源受限环境下,如何高效利用每一字节内存,同时满足功能安全(FuSa)要求,就成为嵌入式开发者必须面对的挑战。
Arm Compiler提供的armlink链接器通过scatter file机制,为开发者提供了精细控制内存布局的能力。这种控制主要体现在三个层面:
一个典型的scatter file由Load Region(加载区域)和Execution Region(执行区域)组成。加载区域定义了程序在存储介质(如Flash)中的布局,而执行区域则描述了运行时内存中的分布。这种分离设计使得开发者可以灵活处理XIP(Execute In Place)等场景。
c复制LOAD_REGION 0x00000000 0x00100000 // 加载区域:起始地址0x0,大小1MB
{
ER_ROM 0x00000000 0x00080000 // 执行区域:Flash中运行
{
*.o (RESET, +FIRST) // 中断向量表必须放在首地址
* (InRoot$$Sections) // 必须放在根区域的特殊段
* (+RO) // 所有只读代码段
}
ER_RAM 0x20000000 0x00020000 // 执行区域:RAM中运行
{
* (+RW, +ZI) // 读写数据和零初始化数据
}
}
实际项目中,我们通常会为关键外设的寄存器映射区保留固定地址。例如,为CAN控制器保留0x40000000开始的4KB空间:
c复制DEVICE_REGION 0x40000000 UNINIT // UNINIT表示不进行初始化 { * (CAN_Registers) // 将CAN寄存器映射段固定在此地址 }
.ANY选择器是armlink提供的高级功能,它允许链接器根据当前内存使用情况,动态决定未明确指定的段应该放置在哪个区域。这种机制特别适合管理大量小型代码段和数据段。
当使用.ANY选择器时,链接器默认采用"最差适应(Worst Fit)"算法:优先将段放入剩余空间最大的区域,以最大化后续分配的灵活性。开发者可以通过--info=any选项查看详细的分配过程:
bash复制armlink --scatter=scatter.scat --info=any -o output.axf input.o
在功能安全(FuSa)认证的项目中,.ANY区域的溢出风险必须特别关注。armlink通过--any_contingency选项提供了应急处理机制:
c复制// 带应急机制的scatter file配置示例
LOAD_REGION 0x0 0x3000
{
ER_1 0x0 0x1000
{
.ANY // 动态分配段
}
// 其他区域...
}
对应的链接命令应包含应急选项:
bash复制armlink --any_contingency --scatter=scatter.scat -o output.axf input.o
当.ANY区域出现分配失败时,链接器会生成类似错误:
code复制Error: L6407E: Sections of aggregate size 0x128 bytes could not fit into .ANY selector(s).
此时应:
对于带有TrustZone技术的Cortex-M23/M33等芯片,armlink提供了自动生成安全网关veneer的功能。这些veneers负责安全与非安全域之间的受控跳转。
典型配置包括:
c复制LOAD_NSCR 0x4000 0x1000 // 非安全可调用区域(Non-Secure Callable)
{
EXEC_NSCR 0x4000 0x1000
{
*(Veneer$$CMSE) // 安全网关veneers
}
}
关键注意事项:
XO(Execute-Only)内存保护是防止代码被恶意读取的有效手段。在scatter file中配置XO区域:
c复制FLASH 0x00000000 0x00080000
{
EXEC_FLASH 0x00000000 0x00080000
{
* (+XO) // Execute-Only代码段
* (+RO) // 普通只读数据
}
}
重要限制:
EMPTY属性允许开发者预留未初始化的内存块,常用于栈和堆管理:
c复制LR_1 0x80000
{
STACK 0x800000 EMPTY -0x10000 // 栈区域:结束于0x800000
{
// 64KB空间,通过负值指定结束地址
}
HEAP +0 EMPTY 0x10000 // 堆区域:紧接着栈区
{
// 64KB空间
}
}
链接器会生成以下符号供程序使用:
在RTOS应用中,我们经常为每个任务栈分配独立EMPTY区域。例如FreeRTOS中可这样配置:
c复制TASK1_STACK 0x20010000 EMPTY -0x800 // 任务1栈:2KB { }
Arm标准库代码的放置需要特别注意。推荐使用InRoot$$Sections确保关键初始化代码位于根区域:
c复制ROM_LOAD 0x0000
{
ROM_EXEC 0x0000
{
vectors.o (Vect, +FIRST) // 中断向量表
* (InRoot$$Sections) // 关键库代码
* (+RO) // 其他只读段
}
}
对于大型项目,可以按库分类放置:
c复制LR1 0x0
{
LIB_C 0x1000 // C库代码
{
*armlib/c_* (+RO)
}
LIB_CPP 0x2000 // C++库代码
{
*libcxx* (+RO)
}
}
L6407E:.ANY区域空间不足
L6220E:执行区域重叠
L6915E:未满足对齐要求
使用--map生成详细内存映射报告
bash复制armlink --scatter=scatter.scat --map -o output.axf input.o
通过--info选项获取特定信息
bash复制armlink --info=sizes,totals --info=any -o output.axf input.o
分析生成的符号表
bash复制fromelf -s output.axf > symbols.txt
版本控制:将scatter file与源码一同纳入版本管理,记录每次内存布局变更的原因
自动化校验:在CI流程中加入链接后检查,验证关键段地址是否符合预期
安全审计:定期检查veneers和CMSE配置,确保安全隔离未被破坏
性能优化:对频繁访问的数据使用FIXED属性固定到快速RAM区域
文档记录:为每个执行区域添加详细注释,说明设计意图和特殊考量
在汽车电子ECU开发中,我们曾遇到因.ANY区域溢出导致的随机崩溃问题。通过引入--any_contingency并预留足够余量,系统稳定性得到显著提升。这也验证了在安全关键系统中,宁可牺牲少量内存利用率,也要确保内存分配的确定性。