AArch64 Tagged Address ABI 深度指南:ARM64 Linux 标签指针与系统调用 ABI 松弛机制
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
本指南基于 Linux 内核源码树中的 Documentation/arch/arm64/tagged-address-abi.rst 编写,系统讲解 AArch64 平台上 Tagged Address ABI(标签地址 ABI)的定义、两级 ABI 松弛规则、prctl()控制接口、例外清单与正确用法,并结合 arch/arm64/kernel/process.c 等源码佐证其实现原理。读完本文,你将掌握:什么是"有效标签指针"、内核在哪些场景允许/禁止带标签指针进入系统调用、如何用PR_SET_TAGGED_ADDR_CTRL在用户态启用该 ABI,以及如何写出可安全复用的标签指针示例代码。
1. 背景:TBI 位与 AArch64 的标签虚拟地址
AArch64 架构中,内核通过配置页表使 TTBR0(即用户态 EL0 的映射)在地址翻译时忽略虚拟地址的最高字节(bits 63:56)。该行为由TCR_EL1.TBI0位控制,且在内核中默认置位。也就是说,用户态天然可以使用"高字节非零"的 64 位指针进行内存访问——这个被解放出来的字节称为 tag(标签),通常与内存标记扩展(MTE, Memory Tagging Extension)等特性配合使用。
关于标签指针在翻译系统中的含义与更底层的细节,可参阅内核文档 Documentation/arch/arm64/tagged-pointers.rst,其中明确指出:内核在解释用户态内存地址时默认假设地址标签为 0x00,除非应用显式启用了 AArch64 Tagged Address ABI。
问题随之而来:尽管 EL0 的访存硬件会忽略最高字节,但内核系统调用接口(syscall ABI)原本要求传入的指针必须是"未打标签"(untagged)的地址。本文所讲的 Tagged Address ABI,就是对这一 syscall 限制的受控松弛(relaxation)。
2. "有效标签指针"(Valid Tagged Pointer)的精确定义
从内核系统调用接口的视角出发,本文档对"有效标签指针"给出如下定义:一个最高字节可能非零、且引用了用户进程地址空间中某一地址的指针,且该地址必须通过以下任一途径获得:
mmap()系统调用,且满足以下任一条件:- flags 中设置了
MAP_ANONYMOUS位;或 - 文件描述符指向一个常规文件(regular file,包括
memfd_create()返回的 fd)或/dev/zero;
- flags 中设置了
brk()系统调用,即进程创建时 program break 的初始位置到当前堆顶之间的堆区域(heap);- 进程创建期间由内核映射到其地址空间中的任意内存(例如 data、bss、stack 段),且受与上述
mmap()相同的限制。
满足上述来源的指针,其高字节打上标签后才被内核承认为"有效标签指针";反之则属于"无效标签指针",其系统调用行为未定义。
3. 两级 ABI 松弛:按"内核是否访问该地址"划分
AArch64 Tagged Address ABI 的松弛分两个阶段,区分标准是内核是否会实际访问(读写)该用户地址。
3.1 第一级:仅用于地址空间管理(内核不访问)
当用户地址不被内核访问、仅用于地址空间管理时(典型场景如mprotect()、madvise()),使用有效标签指针是被允许的。但存在以下例外:
brk()、mmap()以及mremap()的new_address参数——因为这些调用存在与既有用户地址别名(alias)冲突的潜在风险,故必须使用未打标签的指针。- 注意:这一行为在v5.6发生了变化,早期内核可能错误地接受了这些调用中的有效标签指针。
UFFDIO_*系列ioctl()的range.start、start与dst参数,作用于通过userfaultfd()获得的文件描述符。原因在于:随后通过读取该 fd 获得的故障地址(fault address)是未打标签的,若此处接受标签指针,会混淆不感知标签的程序。- 注意:这一行为在v5.14发生了变化,早期内核可能错误地接受了这些调用中的有效标签指针。
3.2 第二级:内核实际访问该地址(需显式开启)
当用户地址会被内核实际访问(典型场景如write())时,ABI 松弛默认关闭,应用线程必须通过prctl()显式开启。这一机制正是本 ABI 的核心接口,详见下一节。
4. 通过 prctl() 控制 ABI:PR_SET/GET_TAGGED_ADDR_CTRL
第二级松弛的开关接口由两个prctl()操作构成,其宏定义位于 include/uapi/linux/prctl.h:
#define PR_SET_TAGGED_ADDR_CTRL 55 #define PR_GET_TAGGED_ADDR_CTRL 56 # define PR_TAGGED_ADDR_ENABLE (1UL << 0)4.1 PR_SET_TAGGED_ADDR_CTRL:启用或禁用
- 原型:
prctl(PR_SET_TAGGED_ADDR_CTRL, unsigned long arg2, 0, 0, 0) arg2是一个位掩码,描述控制模式:PR_TAGGED_ADDR_ENABLE(bit 0):开启 AArch64 Tagged Address ABI。默认状态为关闭。
arg3、arg4、arg5必须为 0。
4.2 PR_GET_TAGGED_ADDR_CTRL:查询状态
- 原型:
prctl(PR_GET_TAGGED_ADDR_CTRL, 0, 0, 0, 0) arg2、arg3、arg4、arg5必须全部为 0,返回值反映当前线程的 ABI 状态(含PR_TAGGED_ADDR_ENABLE等标志位)。
4.3 线程作用域与生命周期
ABI 属性具有以下生命周期特征:
- 线程作用域(thread-scoped):每个线程独立维护自己的 ABI 状态;
- 随 clone() 与 fork() 继承:子进程/线程创建时继承父方的设置;
- 在 exec() 时清除:执行新程序后会重置为默认的关闭状态。
4.4 sysctl 全局开关:abi.tagged_addr_disabled
内核还提供一个全局 sysctl 开关用于兜底禁用:
- 当
sysctl abi.tagged_addr_disabled=1时,调用prctl(PR_SET_TAGGED_ADDR_CTRL, PR_TAGGED_ADDR_ENABLE, 0, 0, 0)会返回-EINVAL; - 默认配置为0(即默认允许启用)。
源码层面的实现位于 arch/arm64/kernel/process.c(受CONFIG_ARM64_TAGGED_ADDR_ABI保护,见 arch/arm64/Kconfig):
static unsigned int tagged_addr_disabled; long set_tagged_addr_ctrl(struct task_struct *task, unsigned long arg) { unsigned long valid_mask = PR_TAGGED_ADDR_ENABLE; struct thread_info *ti = task_thread_info(task); if (is_compat_thread(ti)) return -EINVAL; if (system_supports_mte()) { valid_mask |= PR_MTE_TCF_SYNC | PR_MTE_TCF_ASYNC \ | PR_MTE_TAG_MASK; if (cpus_have_cap(ARM64_MTE_STORE_ONLY)) valid_mask |= PR_MTE_STORE_ONLY; } if (arg & ~valid_mask) return -EINVAL; /* Do not allow the enabling of the tagged address ABI if globally * disabled via sysctl abi.tagged_addr_disabled. */ if (arg & PR_TAGGED_ADDR_ENABLE && tagged_addr_disabled) return -EINVAL; if (set_mte_ctrl(task, arg) != 0) return -EINVAL; update_ti_thread_flag(ti, TIF_TAGGED_ADDR, arg & PR_TAGGED_ADDR_ENABLE); return 0; }从源码可以看到几个关键实现事实:
- 兼容 32 位(COMPAT)线程直接返回
-EINVAL,即该 ABI 仅对 64 位 AArch64 线程有效; - 当系统支持 MTE 时,
valid_mask还会扩展出PR_MTE_TCF_SYNC、PR_MTE_TCF_ASYNC、PR_MTE_TAG_MASK等 MTE 相关控制位,说明该接口同时承担 MTE 线程控制的职责; - 实际生效状态通过设置线程标志
TIF_TAGGED_ADDR记录,读取侧get_tagged_addr_ctrl()则通过test_ti_thread_flag()还原状态; - sysctl 通过
register_sysctl("abi", ...)注册,proc_dointvec_minmax保证取值范围为 0/1(见同文件 arch/arm64/kernel/process.c); - 该 sysctl "只阻止新启用,不撤销已启用线程"——注释明确指出它不会对已 opt-in 的任务生效。
5. 启用后的行为保证
当线程启用了 AArch64 Tagged Address ABI 后,内核保证以下行为:
- 除第 6 节所列例外外,所有系统调用都可接受任何有效标签指针;
- 对于无效标签指针,系统调用行为未定义:可能返回错误码、引发(致命)信号,或以其他方式失败;
- 对于有效标签指针,系统调用行为与对应的未打标签指针完全相同(即标签被安全剥除后按常规处理)。
6. ABI 例外清单:无论是否启用都必须未打标签的参数
以下系统调用参数无论如何都必须使用未打标签的指针,即不受 ABI 松弛影响:
prctl()(指向用户数据的指针除外——即直接被内核访问的指针参数除外);ioctl()(同上,指向用户数据的指针除外);shmat()与shmdt();brk()(自内核 v5.6 起);mmap()(自内核 v5.6 起);mremap()的new_address参数(自内核 v5.6 起)。
对上述参数使用非零标签指针,可能返回错误码、引发(致命)信号或以其他方式失败。
值得补充的是,标签地址的"剥除"机制在内核中由untagged_addr()等辅助宏实现(参见 arch/arm64/include/asm/memory.h),而系统调用入口经由 arch/arm64/include/asm/syscall_wrapper.h 的__SYSCALL_DEFINEx包装宏统一处理寄存器参数,因此在 ABI 生效时,内核可对被访问的用户指针统一做去标签处理。
7. 正确用法示例(完整可编译 C 代码)
以下是文档给出的完整示例,演示如何启用 ABI、分配带标签的内存并执行带标签指针的系统调用:
#include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/mman.h> #include <sys/prctl.h> #define PR_SET_TAGGED_ADDR_CTRL 55 #define PR_TAGGED_ADDR_ENABLE (1UL << 0) #define TAG_SHIFT 56 int main(void) { int tbi_enabled = 0; unsigned long tag = 0; char *ptr; /* check/enable the tagged address ABI */ if (!prctl(PR_SET_TAGGED_ADDR_CTRL, PR_TAGGED_ADDR_ENABLE, 0, 0, 0)) tbi_enabled = 1; /* memory allocation */ ptr = mmap(NULL, sysconf(_SC_PAGE_SIZE), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (ptr == MAP_FAILED) return 1; /* set a non-zero tag if the ABI is available */ if (tbi_enabled) tag = rand() & 0xff; ptr = (char *)((unsigned long)ptr | (tag << TAG_SHIFT)); /* memory access to a tagged address */ strcpy(ptr, "tagged pointer\n"); /* syscall with a tagged pointer */ write(1, ptr, strlen(ptr)); return 0; }对该示例的逐步拆解:
- 启用 ABI:
prctl(PR_SET_TAGGED_ADDR_CTRL, PR_TAGGED_ADDR_ENABLE, 0, 0, 0),返回 0 表示启用成功,程序据此记录tbi_enabled; - 分配内存:使用
mmap()匿名映射一页内存——匿名映射正是第 2 节"有效标签指针"定义的合法来源之一; - 打标签:
TAG_SHIFT为 56,即标签位于最高字节(bits 63:56);若 ABI 可用,则随机生成 8 位标签与地址做按位或;若 ABI 不可用,tag保持 0,行为退化为普通未打标签指针; - 带标签访存:
strcpy(ptr, ...)直接通过带标签地址写内存——硬件因 TBI 忽略最高字节,访存本身不受影响; - 带标签系统调用:
write(1, ptr, strlen(ptr))将带标签指针直接传给内核——这正是第二级 ABI 松弛的典型场景,内核会先剥除标签再访问用户缓冲区。
需要特别强调的是(依据 Documentation/arch/arm64/tagged-pointers.rst 的警示):C 编译器通常不会对仅在高字节上不同的两个虚拟地址做别名规避,因此使用标签指针时必须格外小心,避免与优化器语义冲突。
8. 与 MTE 的关系及版本演进小结
本 ABI 与 ARMv8.5 引入的 MTE 紧密相关:从 arch/arm64/kernel/process.c 可见,当系统支持 MTE 时,set_tagged_addr_ctrl()的合法位掩码会扩展出 MTE 控制位(PR_MTE_TCF_SYNC、PR_MTE_TCF_ASYNC、PR_MTE_TAG_MASK、PR_MTE_STORE_ONLY),即同一prctl()入口同时承担 MTE 标签检查模式与标签掩码的线程级配置;get_tagged_addr_ctrl()也会通过get_mte_ctrl()将这些位一并返回(arch/arm64/kernel/process.c)。同时 MTE 相关配置在 arch/arm64/Kconfig 中依赖ARM64_TAGGED_ADDR_ABI,可见二者在配置层面的耦合关系。
最后,将本 ABI 的语义演进按内核版本汇总:
| 版本 | 变化 |
|---|---|
| 引入 | 定义 AArch64 Tagged Address ABI 及PR_SET/GET_TAGGED_ADDR_CTRL接口 |
| v5.6 | brk()、mmap()、mremap(new_address)改为必须未打标签(早期内核可能错误接受) |
| v5.14 | userfaultfd()上的UFFDIO_*ioctl 参数改为必须未打标签(早期内核可能错误接受) |
说明:以上版本信息以本文档(2019-08-21 撰写)中的注明为准,具体行为请以你所用内核的实际实现为准。
9. 进一步阅读
- Documentation/arch/arm64/tagged-pointers.rst:标签虚拟地址在 AArch64 Linux 中的总体说明(含信号传递时标签保留、
SA_EXPOSE_TAGBITS、sigcontext.fault_address等细节); - include/uapi/linux/prctl.h:
PR_SET/GET_TAGGED_ADDR_CTRL与PR_TAGGED_ADDR_ENABLE的 UAPI 定义; - arch/arm64/kernel/process.c:
set_tagged_addr_ctrl()/get_tagged_addr_ctrl()与abi.tagged_addr_disabledsysctl 的实现; - arch/arm64/Kconfig:
CONFIG_ARM64_TAGGED_ADDR_ABI配置项; - arch/arm64/include/asm/memory.h:
untagged_addr()去标签宏实现。
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考