文章目录
- 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、RAM:
MEMORY { 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是0x38000000,Secure RAM 大小为总 RAM 的一半,也就是0x100000字节。
S_ROM_ALIAS()、S_RAM_ALIAS()在region_defs.h中把 Flash 和 RAM 的偏移转换成 Secure 地址。链接脚本只使用S_CODE_START和S_DATA_START,具体地址由平台配置决定。
PHDRS 和 FLAGS
TF-M 使用PHDRS给不同的输出段建立 ELF load segment:
PHDRS { 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。入口必须位于配置为 NSC(Non-Secure Callable)的区域中,而且第一条指令必须是SG(Secure 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 镜像中。
链接脚本怎样放置 veneer
platform/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)®ION_NAME(Image$$,ER_VENEER,$$Base),(uint32_t)®ION_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_VECTORS | 0x10080400 | 0x1C0 | Secure 向量表 |
.gnu.sgstubs | 0x10080640 | 0x40 | CMSE veneer |
.ER_UNPRIV_CODE | 0x10080680 | 0x1782 | 共享和非特权代码 |
.ER_APP_ROT_CODE | 0x10081E20 | 0x1AA0 | Application RoT 代码 |
.ER_PSA_ROT_CODE | 0x100838C0 | 0x1AD94 | PSA RoT 代码 |
.ER_TFM_CODE | 0x1009E65C | 0x5982 | TF-M 核心代码和 copy/zero table |
.ER_RO_DATA | 0x100A3FE0 | 0x1B04 | 只读数据 |
.TFM_SP_LOAD_LIST | 0x100A5B00 | 0x18C | 分区加载描述 |
这些地址取自build_an521_ninja/bin/tfm_s.map,代码和数据的起始地址会随配置和分区变化。