Python AArch64 分支保护实战:-X perf_jit性能剖析集成中的 BTI/PAC 汇编跳板
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
CPython 的-X perf_jit选项会让解释器把运行时生成的机器码通过 perf jitdump 接口暴露给 Linuxperf分析工具,而 AArch64 平台上用于该机制的汇编跳板(trampoline)需要正确插入 BTI(Branch Target Identification)与 PAC(Pointer Authentication Code)指令。本文围绕 CPython 近期一项核心变更——"Add branch protections for AArch64 (BTI/PAC) in assembly code used by-X perf_jit"(变更说明见 Misc/NEWS.d/next/Core_and_Builtins/2026-05-12-16-47-23.gh-issue-139808.iIs7_E.rst)展开,完整讲清 jitdump 工作原理、AArch64 跳板汇编的 BTI/PAC 条件编译逻辑、GNU property 段声明,以及这些保护指令对 DWARF 栈展开信息生成的连带影响,帮助你在 Arm 服务器上正确启用并理解 Python 性能剖析链路。
-X perf_jit与 perf jitdump:变更所处的技术场景
要理解这条 NEWS 条目,先要弄清它保护的是哪段代码。-X perf_jit是 CPython 的命令行选项,用于启用与 Linuxperf剖析器的集成(选项说明见 Doc/using/cmdline.rst 第 646 行附近,完整使用指南见 Doc/howto/perf_profiling.rst)。启用后,Python 会在进程运行时动态生成的机器码(如 JIT 跳板)被perf采样时,能够被正确解析为带符号、可栈回溯的代码。
其底层是 Linux perf 的jitdump 接口,实现位于 Python/perf_jit_trampoline.c。核心机制可以概括为:
- jitdump 文件:进程首次需要记录 JIT 代码时,
perf_map_jit_init()创建/tmp/jit-<PID>.dump文件,并写入魔数0x4A695444("JiTD")、格式版本、ELF 目标架构、时间戳等头部信息; - 内存映射作为"信号":该文件的第一页以
PROT_READ | PROT_EXEC权限mmap进进程地址空间——perf正是通过扫描/proc/.../maps中匹配 jitdump 命名模式的映射来判断目标进程是否在使用该接口(注意 macOS 上为兼容 samply 跳过了这一步映射); - 事件写入:每生成一段 JIT 代码,就写入一个
PerfUnwindingInfo事件(含 DWARF.eh_frame数据,供--call-graph dwarf栈回溯使用)和一个PerfLoad事件(含虚拟地址、代码尺寸、函数名以及机器码字节本身),函数名按py::<函数名>:<文件名>格式生成,便于在混合语言剖析结果中区分 Python 函数; - 合成 DSO 的地址布局:
perf inject -j会为每段 JIT 代码合成 ELF 文件(/tmp/jitted-PID-N.so),包含 ELF 头、.text与 unwind 信息。为避免地址区间互相覆盖导致样本归属混乱,CPython 在代码区域之间加入固定 padding(trampoline_api.code_padding,按 unwind 数据尺寸 16 字节对齐计算),保证每个合成 DSO 的地址范围互不重叠。
Doc/howto/perf_profiling.rst给出的典型使用方式为(perf record需--call-graph dwarf才能在 JIT 代码内正确展开调用栈):
$ perf record -F 9999 -g -k 1 --call-graph dwarf -o perf.data python -Xperf_jit my_script.py也可以设置环境变量PYTHON_PERF_JIT_SUPPORT达到与-X perf_jit相同的效果(见 Doc/howto/perf_profiling.rst 第 246 行附近)。
在 AArch64 上,jitdump 记录的主要对象就是跳板(trampoline)——解释器与 JIT 生成的代码跳转到符号或相互调用时,超过指令编码距离范围的目标需要一个中转跳板。这次变更正是让AArch64 跳板汇编与内核启用 BTI/PAC 时的分支保护要求保持一致。
AArch64 跳板汇编:结构与设计
跳板实现位于 Python/asm_trampoline_aarch64.S。在 Linux 上导出的符号是_Py_trampoline_func_start/_Py_trampoline_func_end(macOS 下为带双下划线的__Py_trampoline_func_start等),函数体极短:
SIGN_LR /* PAC 签名 LR(条件存在) */ stp x29, x30, [sp, -16]! /* 保存帧指针 x29 与链接寄存器 x30 */ mov x29, sp /* 建立帧指针 */ blr x3 /* 经 x3 中保存的目标地址间接调用 */ ldp x29, x30, [sp], 16 /* 恢复 x29、x30 */ VERIFY_LR /* PAC 校验 LR(条件存在) */ ret /* 返回 */SIGN_LR/VERIFY_LR是本次变更引入的条件汇编宏:签名插入在保存寄存器之前,校验位于ret之前、恢复 x30 之后,位置选择与 PAC 的autiasp(authenticate and add implicit SP)语义严格匹配。
该汇编文件只在__aarch64__ && __AARCH64EL__ && !__ILP32__(64 位 ARM 小端、非 ILP32)条件下编译,并在 Linux 目标末尾附加.note.GNU-stack段标记栈不可执行——这是安全加固(PIE/RELRO 场景下避免可执行栈)的常规做法。
BTI:分支目标标识指令的按需插入
BTI 是 Armv8.5-A 引入的分支目标标识机制:开启 BTI 的 CPU 要求所有间接分支(br/blr等)的目标地址处必须放置bti指令,否则触发分支预测异常。
汇编文件顶部用编译器特征宏条件启用(第 7–15 行):
#if defined(__ARM_FEATURE_BTI_DEFAULT) && __ARM_FEATURE_BTI_DEFAULT == 1 #define BTI_J hint 36 /* bti j: for jumps, IE br instructions */ #define BTI_C hint 34 /* bti c: for calls, IE bl instructions */ #define GNU_PROPERTY_AARCH64_BTI 1 /* bit 0 GNU Notes is for BTI support */ #else #define BTI_J #define BTI_C #define GNU_PROPERTY_AARCH64_BTI 0 #endif要点:
- 用
hint助记符而非bti助记符书写,是为了保证汇编器在没有 BTI 指令支持的版本上也能编译通过,由特征宏__ARM_FEATURE_BTI_DEFAULT(由-mbranch-protection=standard等编译选项触发)决定是否实际生效; bti c(BTI_C)针对调用类分支(bl/blr),bti j(BTI_J)针对跳转类分支(br)。由于该跳板只被blr类调用目标使用,实际生效的是BTI_C——在 PAC 不可用的回退路径中,SIGN_LR直接退化为BTI_C(见下文)。
PAC:指针认证对链接寄存器的签名与校验
PAC(Pointer Authentication Code)用 CPU 内的密钥对寄存器值做认证签名,可抵御返回地址篡改。AArch64 上最常见的是对 LR(x30)的paciasp/autiasp配对。汇编文件中的条件编译(第 17–30 行):
#if defined(__ARM_FEATURE_PAC_DEFAULT) #if __ARM_FEATURE_PAC_DEFAULT & 1 #define SIGN_LR hint 25 /* paciasp: sign with the A key */ #define VERIFY_LR hint 29 /* autiasp: verify with the A key */ #elif __ARM_FEATURE_PAC_DEFAULT & 2 #define SIGN_LR hint 27 /* pacibsp: sign with the b key */ #define VERIFY_LR hint 31 /* autibsp: verify with the b key */ #endif #define GNU_PROPERTY_AARCH64_POINTER_AUTH 2 /* bit 1 GNU Notes is for PAC support */ #else #define SIGN_LR BTI_C #define VERIFY_LR #define GNU_PROPERTY_AARCH64_POINTER_AUTH 0 #endif可以看到三层递进:
__ARM_FEATURE_PAC_DEFAULT的低两位分别指示运行时默认启用A 密钥(paciasp/autiasp)还是B 密钥(pacibsp/autibsp)签名,汇编据此选择hint编码;- 回退路径:如果只有 BTI 而 PAC 不可用,
SIGN_LR退化为BTI_C(仍然提供分支目标保护),VERIFY_LR为空(无签名可验证); - 两个特性都不可用时,
SIGN_LR/VERIFY_LR均为空操作,跳板退化为最基础的保存—跳转—恢复序列,行为与未启用分支保护前一致。
这一设计保证了同一份跳板汇编能编译进不同-mbranch-protection等级的构建(none/standard/full),且与python主可执行文件自身的分支保护属性保持一致——否则动态加载/直接调用跳板时可能因保护属性不匹配而触发异常。
GNU property 段:向动态链接器声明保护能力
仅插入bti/paciasp指令还不够,进程的PT_GNU_PROPERTY动态段必须声明对应的 Arm64 特性,内核与动态链接器才会在执行跳板时启用相应硬件机制。汇编文件末尾(第 63–76 行)手工构造了这个 ELF note:
#if GNU_PROPERTY_AARCH64_BTI != 0 || GNU_PROPERTY_AARCH64_POINTER_AUTH != 0 || GNU_PROPERTY_AARCH64_GCS != 0 .pushsection .note.gnu.property, "a"; .balign 8; .long 4; .long 0x10; .long 0x5; /* NT_GNU_PROPERTY_TYPE_0 */ .asciz "GNU"; .long 0xc0000000; /* GNU_PROPERTY_AARCH64_FEATURE_1_AND */ .long 4; .long (GNU_PROPERTY_AARCH64_BTI|GNU_PROPERTY_AARCH64_POINTER_AUTH|GNU_PROPERTY_AARCH64_GCS); .long 0; .popsection; #endif其中0xc0000000是GNU_PROPERTY_AARCH64_FEATURE_1_AND属性类型,数据字是三个 bit 的按位或:bit 0 = BTI,bit 1 = PAC,bit 2 = GCS(Guarded Control Stack,代码同样预留了__ARM_FEATURE_GCS_DEFAULT的检测)。只有当至少一个特性实际启用时才输出该段,未启用分支保护的构建不会携带多余声明。文件头注释中引用的 Arm ABI 规范文档(AAELF64)说明了这些 GNU Note 的标准语义。
连带影响:DWARF 展开信息必须感知 BTI/PAC 指令
这是容易被忽视、但对perf --call-graph dwarf正确性至关重要的一环。jitdump 文件中每个代码段都附带由 Python/jit_unwind.c 运行时生成的 DWARF.eh_frame(CIE + FDE),展开描述必须与真实指令序列一一对应。由于SIGN_LR/VERIFY_LR是否存在是编译期决定的,jit_unwind.c中的 FDE 生成逻辑同样用相同的特征宏条件分支(Python/jit_unwind.c):
#elif defined(__aarch64__) && defined(__AARCH64EL__) && !defined(__ILP32__) /* AArch64 calling convention unwinding rules */ #if defined(__ARM_FEATURE_PAC_DEFAULT) || \ (defined(__ARM_FEATURE_BTI_DEFAULT) && __ARM_FEATURE_BTI_DEFAULT == 1) DWRF_U8(DWRF_CFA_advance_loc | 1); // Advance past SIGN_LR (4 bytes) #endif #if defined(__ARM_FEATURE_PAC_DEFAULT) DWRF_U8(DWRF_CFA_AARCH64_negate_ra_state); // Saved LR is PAC-signed from here #endif DWRF_U8(DWRF_CFA_advance_loc | 1); // Advance by 1 instruction (4 bytes) DWRF_U8(DWRF_CFA_def_cfa_offset); // CFA = SP + 16 ...两个细节值得注意:
- 当 PAC 或 BTI 任一特性存在时,展开序列先
advance_loc 1(4 字节)跨过SIGN_LR指令,否则 CFA 偏移计算会与stp x29, x30, [sp, -16]!错位 4 字节; - 当启用 PAC 时,插入两条
DWRF_CFA_AARCH64_negate_ra_state(DWARF 5 的 Arm64 专有操作码):保存 x30 时 LR 处于已签名状态(置位),从栈上恢复后签名被消耗(清除)。展开器据此正确处理autiasp与ret之间的关系。
同样的编译期一致性也体现在 Python/perf_jit_trampoline.c 初始化时按_PyJitUnwind_EhFrameSize(0)实测 EH 数据尺寸来计算code_padding——EH 数据因 BTI/PAC 指令的存在而变大,padding 随之自动适配,无需人工维护常量(文件头注释中提到的历史常量值 0x50 即由此机制取代)。
与 JIT 跳板 arena 的关系
除了asm_trampoline_aarch64.S中的通用跳板函数,JIT 代码页内部还会按目标符号动态生成跳板槽位:Python/jit.c 中的patch_aarch64_trampoline()从跳板 arena 中取TRAMPOLINE_SIZE大小的槽位并逐字写入跳转指令(第 572 行附近),跳板总数按符号掩码统计(第 657–667 行),并计入代码页尺寸与jit_trampoline_size统计。这些 JIT 代码页在-X perf_jit下正是通过上文_PyPerfJit_WriteNamedCode→ jitdump 事件被perf记录的路径,因此汇编跳板的分支保护属性会沿"arena 跳板 + 汇编跳板 + jitdump 展开信息"整条链路生效。
验证路径与相关测试
仓库中与该集成相关的测试入口:
- Lib/test/test_perf_profiler.py:验证 Linux perf jitdump 集成(生成与解析 jitdump 文件);
- Lib/test/test_samply_profiler.py:验证 macOS 上 samply 消费同一 jitdump 接口的场景(该测试解释了源码中 macOS 跳过 mmap 信号、时间戳改用
mach_absolute_time()时钟域等分支); - 命令行选项本身的行为在 Lib/test/test_cmd_line.py 中覆盖
-X perf_jit的解析。
在真实 AArch64 系统上验证 BTI/PAC 是否按预期生效,可以从两个层面入手(均为只读检查):一是用readelf -n查看 python 二进制的GNU Property段,确认AARCH64_FEATURE_1的 bit 0/bit 1 置位情况,与编译时的__ARM_FEATURE_BTI_DEFAULT/__ARM_FEATURE_PAC_DEFAULT一致;二是运行perf record -F 9999 -g -k 1 --call-graph dwarf -o perf.data python -Xperf_jit my_script.py后检查perf report能否在py::前缀的符号上正确展开调用栈——若展开描述与实际指令错位,通常正是jit_unwind.c与汇编跳板的条件编译宏不同步所致。
小结
这条 NEWS 条目对应的变更看似只是"给汇编加两条指令",实际牵动四处代码保持严格的编译期同步:跳板汇编中的SIGN_LR/VERIFY_LR条件宏(Python/asm_trampoline_aarch64.S)、.note.gnu.property段的特性位声明、运行时 DWARF FDE 生成的advance_loc与negate_ra_state序列(Python/jit_unwind.c),以及 jitdump padding 按 EH 尺寸自适应的计算(Python/perf_jit_trampoline.c)。理解这条链路,你才能在启用-mbranch-protection的 Arm 服务器上放心使用-X perf_jit做 Python 性能剖析,并能在展开异常时快速定位是汇编、GNU property 还是展开描述三者的哪一侧失配。
【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考