在嵌入式安全应用开发中,内存布局的精确控制直接关系到系统的可靠性和安全性。Arm Compiler的链接器通过scatter-loading机制,为开发者提供了细粒度的内存映射控制能力。不同于桌面应用的自动内存分配,嵌入式系统往往需要手动规划不同属性代码段的物理位置,这主要基于三个关键考量:
首先,嵌入式设备的存储架构通常采用分层的设计。比如STM32系列MCU将Flash和SRAM物理隔离,Flash存放RO代码和常量,SRAM存放RW数据和ZI区。这种隔离要求链接器必须明确知道各段的加载地址和执行地址。
其次,安全关键系统对内存保护有严格要求。例如,将可执行代码与数据段分离可以防止数据区被恶意执行,而XO(Execute-Only)区域的引入则能进一步防止代码被读取。Armv8-M的TrustZone技术更是需要精确的region划分来实现安全隔离。
第三,性能优化需求。将频繁访问的数据放在紧耦合存储器(TCM)或核心附近的SRAM能显著提升性能,而冷数据可以存放在外部SDRAM。这需要链接器支持非连续区域的配置。
加载视图(Load View)与执行视图(Execution View):
内存属性分类:
地址属性:
Type 2模型是嵌入式系统中最常用的内存布局方式,其特征是单一加载区域对应多个非连续的执行区域。这种模型适合大多数具有独立Flash和RAM的微控制器。
以下是一个典型的Type 2 scatter文件配置,对应命令行参数--ro_base=0x010000 --rw_base=0x040000:
c复制LR_1 0x010000 ; 加载区域LR_1起始于0x010000
{
ER_RO +0 ; RO执行区域从加载区域起始地址开始
{
* (+RO) ; 所有RO段集中存放
}
ER_RW 0x040000 ; RW执行区域固定在0x040000(通常是SRAM)
{
* (+RW) ; 所有RW段集中存放
}
ER_ZI +0 ; ZI区域紧接RW区域之后
{
* (+ZI) ; 所有ZI段集中存放
}
}
--ro_base:
--rw_base:
地址继承规则:
对于支持Execute-Only内存的Armv7-M/Armv8-M架构,配置需特别处理:
c复制LR_1 0x010000
{
ER_XO +0 ; XO区域必须放在最前面
{
* (+XO)
}
ER_RO +0 ; 常规RO区域紧随XO之后
{
* (+RO)
}
/* RW/ZI区域配置不变 */
}
重要提示:XO区域不能与ROPI(position-independent)同时使用,否则链接器会报错。在Armv6-M架构上XO功能不可用。
Type 3模型进一步将加载区域分割,允许RO、RW等段存放在完全独立的存储设备中。这种模型适用于:
以下示例对应命令行参数--ro_base=0x010000 --rw_base=0x040000 --split:
c复制LR_1 0x010000 ; RO加载区域
{
ER_RO +0
{
* (+RO)
}
}
LR_2 0x040000 ; RW加载区域
{
ER_RW +0
{
* (+RW)
}
ER_ZI +0
{
* (+ZI)
}
}
RELOC属性生成包含重定位信息的目标文件,适合后续由bootloader进行二次加载:
c复制LR_1 0x010000 RELOC
{
ER_RO +0
{
* (+RO)
}
}
LR2 0x040000 RELOC
{
ER_RW +0
{
* (+RW)
}
ER_ZI +0
{
* (+ZI)
}
}
重定位信息包括:
当系统需要XO保护且存储介质分离时:
c复制LR_XO 0x010000 ; XO专用加载区域
{
ER_XO +0
{
* (+XO)
}
}
LR_RO 0x020000 ; 常规RO区域
{
ER_RO +0
{
* (+RO)
}
}
/* RW/ZI区域配置同上 */
当多个执行区域匹配同一输入段时,链接器按以下优先级处理:
冲突解决示例:
c复制ER_ROM 0x040000
{
startup.o (+ENTRY) ; 入口代码
}
ER_TEXT 0x041000
{
* (+RO-CODE) ; 其他代码段
}
在此例中,startup.o中的ENTRY段会优先放入ER_ROM,即使它也匹配ER_TEXT的+RO-CODE属性。
RWPI配置示例:
c复制LR_1 0x010000
{
ER_RO +0 ; 绝对地址RO
{
* (+RO)
}
ER_RW 0x018000 PI ; 位置无关RW
{
* (+RW)
}
ER_ZI +0 ; 继承PI属性
{
* (+ZI)
}
}
关键限制:
问题1:ZO区域地址溢出
问题2:XO区域配置错误
问题3:重定位失败
关键数据隔离:
c复制ER_SECURE 0x10000000 0x1000
{
secure_data.o (+RW)
}
代码保护措施:
启动安全检查:
工具链安全配置:
bash复制armlink --no_merge --no_merge_litpools --protect
热代码布局:
c复制ER_ITCM 0x00000000
{
critical.o (+RO-CODE)
}
数据对齐优化:
c复制ER_RAM ALIGN 32
{
* (+RW-DATA)
}
压缩与尺寸优化:
在实际项目中,我曾遇到一个因ZI区域未正确清零导致的随机崩溃问题。通过分析生成的map文件,发现链接器虽然正确设置了ZI区域地址和大小,但启动代码中对应的清零循环被优化器意外移除。解决方法是在scatter文件中明确标注ZI区域:
c复制ER_ZI +0 EMPTY 0x800 ; 明确指定ZI区域大小
{
* (+ZI)
}
并在启动代码中添加__attribute__((optimize("O0")))防止关键初始化代码被优化。这个案例凸显了链接器配置与启动代码协同工作的重要性。