rustc E0798 深度解析:cmse-nonsecure-callABI 的参数与返回值寄存器约束
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
E0798 是 rustc 针对 ARMv8-M TrustZone 安全扩展下cmse-nonsecure-call(以及cmse-nonsecure-entry)ABI 施加的签名合法性校验错误:当被调用方与调用方处于不同安全状态、只能通过寄存器而非栈传递数据时,编译器要求函数指针的输入必须装进 4 个 32 位参数寄存器、返回值必须 ≤ 4 字节或为 8 字节基础标量类型、签名中不得出现泛型。本文以 E0798.md 为主体,结合rustc_hir_analysis中的校验实现、rustc_abi中的 ABI 定义与tests目录下的回归测试,逐条拆解这三条规则的成因、错误触发场景与修复方法。
E0798 是什么:TrustZone 非安全调用 ABI 的"极受限"签名约束
在 ARMv8-M 的 TrustZone 安全模型中,安全世界(Secure World)与非安全世界(Non-secure World)通过 SG 指令与cmse-nonsecure-call函数指针完成受控调用。由于调用过程中参数与返回值必须全部经由寄存器传递、严禁溢出(spill)到栈上(栈可能处于不可信的非安全内存),rustc 为这类 ABI 定义了极其严格的签名规则。
这一点在 extern_abi.rs 的枚举定义中表述得非常直白:CmseNonSecureCall与CmseNonSecureEntry被注释为"extremely constrained barely-C ABI for TrustZone"(面向 TrustZone 的、约束极其严格的准 C ABI),对应字符串分别为"cmse-nonsecure-call"与"cmse-nonsecure-entry"(见 extern_abi.rs)。
E0798 的具体内容归纳为三条硬性限制(原文见 E0798.md):
- 输入参数必须能放入 4 个可用的 32 位参数寄存器(即 r0–r3,总计 16 字节),且对齐(alignment)参与计算;
- 返回值必须 ≤ 4 字节,或者是一个 8 字节的基础类型(
i64、u64、f64); - 签名中不允许出现泛型。
注意:这些规则仅作用于函数指针类型。
cmse-nonsecure-callABI 只允许出现在函数指针上,若将其用于函数定义本身,会触发另一个错误码 E0781(详见下文"触发路径"一节)。
触发 E0798 的典型场景一:参数超出 4 个寄存器
当函数指针的参数数量或单个参数的尺寸超过 16 字节总量时,rustc 直接报 E0798。原文档给出的第一个错误示例:
#![feature(abi_cmse_nonsecure_call)] #[no_mangle] pub fn test( f: extern "cmse-nonsecure-call" fn(u32, u32, u32, u32, u32) -> u32, ) -> u32 { f(1, 2, 3, 4, 5) }五个u32共 20 字节,超过 r0–r3 合计的 16 字节容量,因此报错。对应的诊断消息在 diagnostics.rs 中定义:
- 主消息:
arguments for "cmse-nonsecure-call" function too large to pass via registers - 附注:
functions with the "cmse-nonsecure-call" ABI must pass all their arguments via the 4 32-bit argument registers - 标签:在溢出的参数上标记
does not fit in the available registers
其中CmseInputsStackSpill诊断结构体的spans字段是Vec<Span>——所有导致溢出的参数都会被依次标记,而不是只报第一个。
触发 E0798 的典型场景二:对齐插入填充,挤占寄存器空间
第二条限制强调"对齐是相关的",这是最容易踩坑的地方。原文档给出的第二个错误示例:
#![feature(abi_cmse_nonsecure_call)] #[no_mangle] pub fn test( f: extern "cmse-nonsecure-call" fn(u32, u64, f32) -> u32, ) -> u32 { f(1, 2, 3.0) }表面上三个参数(4 + 8 + 4 = 16 字节)刚好"塞满"四个寄存器,但 AAPCS32 的规则要求 8 字节对齐的类型必须从偶数编号寄存器开始。于是:
u32放入 r0;u64需要 8 字节对齐,必须占用r2 与 r3,r1 被填充(padding)浪费;- 剩余的
f32已无寄存器可放,报 E0798。
源码中的对齐累加算法
这段规则在 cmse.rs 的is_valid_cmse_inputs中由一段精炼的"虚拟寄存器分配"实现:
let align = layout.layout.align().bytes(); let size = layout.layout.size().bytes(); accum += size; accum = accum.next_multiple_of(Ord::max(4, align)); // i.e. exceeds 4 32-bit registers if accum > 16 { excess_argument_spans.push(hir_ty.span); }关键在第二行:每处理完一个参数,累计字节数accum都要向上对齐到max(4, align)。u64的 align 为 8,因此它在 r0 的u32之后需要把累计值从 4 提升到 8 的倍数(即 8),等效于在 r1 位置插入 4 字节填充——这与原文档"padding is inserted so that theu64argument is passed in registers r2 and r3"的描述完全吻合。该实现还基于tcx.layout_of查询真实内存布局(见 cmse.rs),因此对齐与尺寸信息与最终代码生成完全一致。
返回值规则:4 字节以内,或透明包装的 8 字节标量
输出侧由is_valid_cmse_output与is_valid_cmse_output_layout校验(cmse.rs),判定逻辑为:
let size = layout.layout.size().bytes(); if size <= 4 { return true; } else if size != 8 { return false; } // Accept (transparently wrapped) scalar 64-bit primitives. matches!( layout.peel_transparent_wrappers(&cx).ty.kind(), ty::Int(ty::IntTy::I64) | ty::Uint(ty::UintTy::U64) | ty::Float(ty::FloatTy::F64) )三条结论由此得出:
- 尺寸 ≤ 4 字节的类型(
u32、f32、指针等)合法; - 尺寸为 8 字节的类型只有三种直接合法:
i64、u64、f64。8 字节的复合类型(如(u32, u32))不合法; - **透明包装(transparent wrapper)**被接受:即通过
#[repr(transparent)]包装的i64/u64/f64结构体也可以作为返回值,校验时会调用peel_transparent_wrappers剥掉包装层再判断底层类型。
对应的诊断结构体是CmseOutputStackSpill(diagnostics.rs),其附注完整复述了这条规则:"the result must either be a (transparently wrapped) i64, u64 or f64, or be at most 4 bytes in size",并在返回类型上标记this type doesn't fit in the available registers。
泛型与impl Trait:签名必须是完全具体化的
第三条限制"no generics can be used in the signature"的触发面比字面意思更广,源码中由两条路径覆盖:
- 泛型(generic 类型参数):布局计算对泛型类型返回
LayoutError::TooGeneric,此时should_emit_layout_error(cmse.rs)会将其转发为CmseGeneric诊断(diagnostics.rs),消息为generics are not allowed in "cmse-nonsecure-call" signatures。因为该 ABI 本就要求参数/返回值按寄存器精确排布,泛型意味着无法确定布局; impl Trait(opaque 类型):对cmse-nonsecure-entry的返回类型,若包含 opaque 类型,直接发出CmseImplTrait诊断(消息"impl Trait" is not allowed in "cmse-nonsecure-call" signatures,见 diagnostics.rs)。cmse.rs 中的注释解释了原因:cmse-nonsecure-call只能出现在函数指针上,函数指针本身不允许impl Trait;而对cmse-nonsecure-entry显式禁止是为了避免布局计算时产生查询环(query cycle),且这类入口函数通常搭配#[no_mangle]使用,泛型/Opaque 类型本身就没有意义。
需要说明的是,E0798同时服务于cmse-nonsecure-call与cmse-nonsecure-entry两个 ABI 的校验,但文档示例聚焦前者;后者多一层"C 变参(c-variadic)直接放行"的例外处理(见 cmse.rs)。
触发路径与诊断实现:校验发生在哪一步
E0798 的完整链路如下:
- 类型检查阶段,
lower_fn_ty在将 HIR 中的函数类型降级为内部ty::PolyFnSig时调用cmse::validate_cmse_abi(...)(调用点见 mod.rs); validate_cmse_abi(cmse.rs)根据 ABI 分发:CmseNonSecureCall:要求对应 HIR 节点必须是函数指针类型(TyKind::FnPtr),否则报E0781——the "cmse-nonsecure-call" ABI is only allowed on function pointers(这也解释了为什么前文示例里cmse-nonsecure-call只能修饰fn类型参数而非函数定义);CmseNonSecureEntry:取函数签名decl,变参直接返回;
- 分别调用
is_valid_cmse_inputs与is_valid_cmse_output做寄存器适配检查,不符合则 emit 对应的 E0798 诊断。
从注释(cmse.rs)可知,LLVM 后端同样会验证这些条件,rustc 提前在类型检查阶段拦截,是为了输出更友好的错误信息。
编译前提与相关测试佐证
启用前提:cmse-nonsecure-call属于 unstable 特性,需在 Nightly 编译器上开启#![feature(abi_cmse_nonsecure_call)]。特性门控登记于 unstable.rs,自 Rust 1.90.0 起提供(关联 issue #81391)。此外,这两个 ABI 面向ARMv8-M 目标(如thumbv8m.main-none-eabi),并依赖 LLVM 的 ARM 组件。
仓库测试从两个角度佐证了这套约束:
- 汇编输出测试tests/assembly-llvm/cmse.rs:针对
thumbv8m.main-none-eabi/eabihf目标编译入口函数,CHECK断言入口处会清空 r0–r3、r12、标志寄存器,硬浮点下还会检测 CONTROL 寄存器的 FPU 位并清空 d0–d7 与 FPSCR——这正是"返回值必须塞进寄存器、不得依赖任何遗留状态"的代码生成体现; - UI 测试tests/ui/explicit-tail-calls/unsupported-abi/cmse-nonsecure-call.rs:验证
cmse-nonsecure-call不能用作函数定义的 ABI(触发 E0781),以及它不支持保证尾调用(guaranteed tail call /become)。
修复 E0798 的实用建议
结合上述规则,遇到 E0798 时按以下思路处理:
- 精简参数:将参数总量压到 16 字节以内,且始终把 8 字节对齐的大类型(
u64、f64等)安排在不会被填充浪费的位置,避免对齐 padding 挤占寄存器(参考原文档第二个示例的反例); - 规整返回值:返回
u32/f32/指针等 ≤ 4 字节类型,或i64/u64/f64(含#[repr(transparent)]包装),不要返回 8 字节的复合类型; - 消除泛型:确保
cmse-nonsecure-call函数指针与cmse-nonsecure-entry入口函数的签名完全具体化,不使用泛型参数与impl Trait返回类型; - 确认目标平台:该 ABI 仅对 ARMv8-M TrustZone 目标有意义,在 x86_64 等宿主目标上无法验证,测试需交叉编译到
thumbv8m.main-none-eabi(正如 tests/assembly-llvm/cmse.rs 顶部--target thumbv8m.main-none-eabi的编译标志所示)。
理解 E0798 的本质——"TrustZone 边界上一切皆寄存器"——就能在设计跨安全域调用签名时提前避开这些坑:这不仅是一个错误码,更是 rustc 对安全关键 ABI 约束的编译期守护。
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考