文章目录
- 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.template
CMake 预处理后实际参与链接的文件是:
build_an521_ninja/generated/platform/ext/common/gcc/tfm_isolation_s.ld
MEMORY 定义 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,代码和数据的起始地址会随配置和分区变化。




