欢迎光临
我们一直在努力

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.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)&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_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,代码和数据的起始地址会随配置和分区变化。

赞(0)
未经允许不得转载:171主机测评 » TF-M Secure 链接脚本常用语法
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址