TF-M Secure 链接脚本常用语法

发布时间:2026/11/4 19:57:09
TF-M Secure 链接脚本常用语法 文章目录TF-M Secure 链接脚本常用语法MEMORY 定义 Secure 区域PHDRS 和 FLAGS输出段和位置标签TrustZone Veneer 段链接脚本怎样放置 veneer平台怎样使用这两个边界分区加载信息表AT FLASH 的数据段NOLOAD 和 Secure 栈分区私有数据和边界对齐Overlay 运行时栈ASSERT 和 CHECK_ALIGNMENT_4当前链接结果TF-M Secure 链接脚本常用语法TF-M Secure 的链接脚本除了安排代码和数据还要描述 TrustZone 区域、分区私有数据、运行时加载表和安全边界。本文只介绍这些和 TF-M Secure 相关的写法。当前使用的模板是platform/ext/common/gcc/tfm_isolation_s.ld.templateCMake 预处理后实际参与链接的文件是build_an521_ninja/generated/platform/ext/common/gcc/tfm_isolation_s.ldMEMORY 定义 Secure 区域TF-M Secure 链接脚本从平台的region_defs.h获取区域宏再把 Secure 代码和 Secure 数据分别放入FLASH、RAMMEMORY { FLASH (rx) : ORIGIN S_CODE_START, LENGTH S_CODE_SIZE RAM (rw) : ORIGIN S_DATA_START, LENGTH S_DATA_SIZE }AN521 当前构建中S_CODE_START是0x10080400对应 Secure 镜像跳过0x400字节 image header 后的地址。S_DATA_START是0x38000000Secure RAM 大小为总 RAM 的一半也就是0x100000字节。S_ROM_ALIAS()、S_RAM_ALIAS()在region_defs.h中把 Flash 和 RAM 的偏移转换成 Secure 地址。链接脚本只使用S_CODE_START和S_DATA_START具体地址由平台配置决定。PHDRS 和 FLAGSTF-M 使用PHDRS给不同的输出段建立 ELF load segmentPHDRS { flash PT_LOAD FLAGS(5); /* R X */ tfm_data PT_LOAD FLAGS(6); /* R W */ phdr_TFM_SP_CRYPTO PT_LOAD FLAGS(6); coderam PT_LOAD FLAGS(5); }FLAGS(5)表示可读、可执行FLAGS(6)表示可读、可写。输出段末尾的:flash、:tfm_data或:phdr_TFM_SP_CRYPTO将该段放入对应的 load segment。这样生成的 ELF 可以把代码、TF-M 全局数据和各分区私有数据分开描述。输出段和位置标签TF-M 不把所有代码都放入一个.text。它按照 PSA RoT、Application RoT、共享代码和只读数据划分区域.ER_UNPRIV_CODE . : { *libtfm_sprt*:(SORT_BY_ALIGNMENT(.text*)) } FLASH :flash .ER_APP_ROT_CODE . : { *tfm_app_rot_partition*:(SORT_BY_ALIGNMENT(.text*)) } FLASH :flash .ER_PSA_ROT_CODE ALIGN(4) (READONLY) : { *tfm_psa_rot_partition*:(SORT_BY_ALIGNMENT(.text*)) } FLASH :flash在每个区域前后脚本用下面类似的符号记录边界Image$$PT_APP_ROT_CODE_START$$Base .; Image$$PT_APP_ROT_CODE_END$$Base .;Image$$...是链接符号不会创建新的数据。TF-M 的 SAU、MPU 和分区边界配置会读取这些符号确定代码和数据区域的起止地址。$$是 TF-M 兼容不同工具链时ARMCC采用的符号命名形式。TrustZone Veneer 段Non-Secure 代码不能直接跳到任意 Secure 函数。要让一个 Secure 函数可以被 Non-Secure 调用需要为它提供一个很短的入口这个入口称为 CMSE veneer。入口必须位于配置为 NSCNon-Secure Callable的区域中而且第一条指令必须是SGSecure Gateway。Non-Secure 代码跳到这个入口执行SG后处理器切换到 Secure 状态再继续执行 Secure 代码。以当前 PSA 接口为例Secure 源码中定义了下面这个函数并通过cmse_nonsecure_entry属性声明它可以供 Non-Secure 调用。secure_fw/partitions/ns_agent_tz/psa_api_veneers_v80m.c tfm_psa_call_veneer()编译器为这个函数生成 Secure 侧的函数体链接器再生成带SG指令的入口。最终在镜像中可以看到两部分代码。代码作用.gnu.sgstubs中的tfm_psa_call_veneer对 NS 开放的入口先执行SG再跳到 Secure 实现Secure 代码区中的__acle_se_tfm_psa_call_veneer真正执行参数处理和 PSA 调用的函数体当前tfm_s.axf中的入口只有两条指令。10080660 tfm_psa_call_veneer: 10080660: e97f e97f sg 10080664: f01e b8ca b.w 0x1009e7fc __acle_se_tfm_psa_call_veneer第一条sg完成安全状态切换第二条b.w跳到0x1009e7fc的 Secure 函数体。这个入口共占 8 字节。Non-Secure 应用需要知道入口地址才能调用它。Secure 镜像链接时会生成导入目标文件s_veneers.o安装步骤再把它导出到api_ns/interface/lib/。用nm查看这个文件可以看到入口的地址符号。10080660 A tfm_psa_call_veneer这里的A表示绝对地址符号。NS 工程链接这个文件后就知道tfm_psa_call_veneer位于0x10080660。NS 侧的psa_call()通过这个入口进入 Secure入口指令和 Secure 函数体都保存在 Secure 镜像中。链接脚本怎样放置 veneerplatform/ext/common/gcc/tfm_isolation_s.ld.template用VENEERS()宏定义这段区域的布局。#define VENEERS() \ .gnu.sgstubs TFM_LINKER_VENEERS_START : \ { \ *(.gnu.sgstubs*) \ } FLASH :flash \ .sgstubs_end : TFM_LINKER_VENEERS_END \ { \ } FLASH :flash先看宏和链接脚本的语法。行尾的\是 C 预处理宏的续行符。构建时先展开这个宏再把生成的链接脚本交给 GNU ld。.gnu.sgstubs是输出段名里面放置带SG的入口。*(.gnu.sgstubs*)匹配所有输入文件中名字以.gnu.sgstubs开头的输入段GNU 链接器生成的入口也会加入这个输出段。 FLASH表示把输出段放到MEMORY中定义的FLASH区域:flash表示把它归入PHDRS中名为flash的 ELF segment。.sgstubs_end是一个空输出段用来确定整个 veneer 区域对齐后的结束地址。TFM_LINKER_VENEERS_START和TFM_LINKER_VENEERS_END默认都展开为ALIGN(TFM_LINKER_VENEERS_ALIGNMENT)。当前构建的对齐值是0x20也就是 32 字节对应 SAU 配置区域边界的粒度。前一个宏对齐入口区域的起点后一个宏对齐空段.sgstubs_end的地址。这里额外放置空段是因为 GNU 链接器会在.gnu.sgstubs末尾追加生成的入口。模板使用下一个空段的起始地址表示对齐后的结束边界平台代码就能拿到包含尾部对齐空间的完整范围。当前 map 文件中的结果如下。.gnu.sgstubs 0x10080640 0x40 linker stubs 0x28 .sgstubs_end 0x10080680 0x0这里一共有 5 个入口每个 8 字节指令共占0x28字节。整个输出段占0x40字节其中最后0x18字节用于对齐。0x10080640 .gnu.sgstubs 起点 5 个 SG B.W 入口共 0x28 字节 0x10080668 入口指令结束 对齐空间共 0x18 字节 0x10080680 .sgstubs_end自身大小为 0脚本随后把区域起点和对齐后的终点导出成链接符号。Image$$ER_VENEER$$Base ADDR(.gnu.sgstubs); Image$$VENEER_ALIGN$$Limit ADDR(.sgstubs_end);在这个构建中两个符号的值分别是0x10080640和0x10080680。Base是第一个字节的地址Limit是区域后面第一个字节的地址所以整个区域是[0x10080640, 0x10080680)包含实际入口和尾部对齐空间。平台怎样使用这两个边界platform/ext/target/arm/mps2/an521/target_cfg.c通过REGION_DECLARE声明这两个链接符号。代码取符号地址就能得到链接脚本给出的区域边界。REGION_DECLARE(Image$$,ER_VENEER,$$Base);REGION_DECLARE(Image$$,VENEER_ALIGN,$$Limit);随后把这两个地址填入 SAU 配置表。{(uint32_t)REGION_NAME(Image$$,ER_VENEER,$$Base),(uint32_t)REGION_NAME(Image$$,VENEER_ALIGN,$$Limit)-1,true,},SAU 的结束地址包含最后一个字节因此这里要用Limit - 1实际配置的范围是0x10080640到0x1008067f。表项最后的true表示把这段区域配置为 NSC。NSC 区域仍属于 Secure 内存只允许 Non-Secure 通过其中合法的SG入口进入。对齐空间不会因为被包含在 NSC 范围里就变成可调用的入口。真正的 Secure 函数体位于这个范围之外Non-Secure 代码不能直接跳进去。把链接和运行两部分连起来一次调用的路径如下。NS psa_call() - tfm_psa_call_veneer 0x10080660 - SG - __acle_se_tfm_psa_call_veneer - Secure SPM 和目标分区分区加载信息表每个 TF-M 分区的自动生成文件都定义一个.part_load对象。链接脚本用KEEP把它们收集到连续的.TFM_SP_LOAD_LIST.TFM_SP_LOAD_LIST ALIGN(TFM_LINKER_PT_RO_DATA_ALIGNMENT) : { KEEP(*(.part_load)) } FLASH :flash Image$$TFM_SP_LOAD_LIST$$RO$$Base ADDR(.TFM_SP_LOAD_LIST); Image$$TFM_SP_LOAD_LIST$$RO$$Limit ADDR(.TFM_SP_LOAD_LIST) SIZEOF(.TFM_SP_LOAD_LIST);KEEP防止链接器因为没有普通代码引用而丢弃分区描述。SPM 在运行时从这段只读区域读取分区、服务、依赖和入口信息。当前 map 文件中的表从0x100A5B00开始到0x100A5C8C结束。AT FLASH 的数据段分区私有的可写初始数据运行在 RAM但初始值保存在 Flash.ER_TFM_SP_CRYPTO_DATA : ALIGN(TFM_LINKER_PT_PROT_PART_PRIVATE_DATA_ALIGNMENT) { *tfm_*partition_crypto.*:(SORT_BY_ALIGNMENT(.data*)) } RAM AT FLASH :phdr_TFM_SP_CRYPTO RAM指定运行地址AT FLASH指定加载地址。生成的 copy table 会记录这两个地址__cmsis_start启动时把初始值复制到 RAM。对应的.ER_TFM_SP_CRYPTO_BSS使用(NOLOAD)只在 RAM 中分配空间由 zero table 清零。NOLOAD 和 Secure 栈不会从镜像加载初值的区域使用NOLOAD。例如 Secure 启动栈.msp_stack ALIGN(TFM_LINKER_MSP_STACK_ALIGNMENT) (NOLOAD) : { . __msp_stack_size__ - STACKSEAL_SIZE; } RAM Image$$ARM_LIB_STACK$$ZI$$Base ADDR(.msp_stack); Image$$ARM_LIB_STACK$$ZI$$Limit ADDR(.msp_stack) SIZEOF(.msp_stack);__INITIAL_SP使用Image$$ARM_LIB_STACK$$ZI$$Limit__STACK_LIMIT使用Image$$ARM_LIB_STACK$$ZI$$Base。Cortex-M33 的 stack seal 单独放在.msp_stack_seal_res所以主栈大小要减去STACKSEAL_SIZE。分区私有数据和边界对齐每个分区的数据和 BSS 使用独立输出段并按对应的链接对齐宏对齐.ER_TFM_SP_ITS_DATA : ALIGN(TFM_LINKER_PT_PROT_PART_PRIVATE_DATA_ALIGNMENT) { *tfm_*partition_its.*:(SORT_BY_ALIGNMENT(.data*)) } RAM AT FLASH :phdr_TFM_SP_ITS .ER_TFM_SP_ITS_BSS (NOLOAD) : ALIGN(TFM_LINKER_PT_PROT_PART_PRIVATE_DATA_ALIGNMENT) { *tfm_*partition_its.*:(SORT_BY_ALIGNMENT(.bss*)) } RAM分区隔离代码使用Image$$PT_TFM_SP_ITS_PRIVATE_DATA_START$$Base和对应的END符号建立 MPU 区域。对齐不仅影响空间大小也保证 MPU 区域边界满足平台要求。Overlay 运行时栈启用CONFIG_TFM_REUSE_COPY_AREA_FOR_SP_STACKS时多个分区可以复用 Flash copy area 对应的 RAM 空间。脚本先用一个空的.flash_marker1记录 copy area 的加载地址再把运行时栈放进 overlay 区域Image$$PT_OVERLAY_AREA_BEGIN$$Base LOADADDR(.flash_marker1); .tfm_runtime_sp_stacks Image$$PT_OVERLAY_AREA_BEGIN$$Base (NOLOAD) : { KEEP(*(.tfm_runtime_sp_stacks*)) } FLASH这里的关键是同时控制运行地址和加载地址的对齐保证分区切换时可以复用同一块栈空间。ASSERT 和 CHECK_ALIGNMENT_4链接脚本会在生成镜像时检查 Secure 向量表和数据段ASSERT(__vectors_start__ ! __vectors_end__, .vectors should not be empty) CHECK_ALIGNMENT_4(SIZEOF(.ER_TFM_SP_CRYPTO_DATA))向量表为空或分区数据没有按 4 字节对齐时链接直接失败。把错误放到链接阶段可以在镜像生成前发现布局问题。当前链接结果当前tfm_s.map的 Secure Flash 区域如下输出段起始地址大小用途.TFM_VECTORS0x100804000x1C0Secure 向量表.gnu.sgstubs0x100806400x40CMSE veneer.ER_UNPRIV_CODE0x100806800x1782共享和非特权代码.ER_APP_ROT_CODE0x10081E200x1AA0Application RoT 代码.ER_PSA_ROT_CODE0x100838C00x1AD94PSA RoT 代码.ER_TFM_CODE0x1009E65C0x5982TF-M 核心代码和 copy/zero table.ER_RO_DATA0x100A3FE00x1B04只读数据.TFM_SP_LOAD_LIST0x100A5B000x18C分区加载描述这些地址取自build_an521_ninja/bin/tfm_s.map代码和数据的起始地址会随配置和分区变化。