rustc E0798 深度解析:`cmse-nonsecure-call` ABI 的参数与返回值寄存器约束
rustc E0798 深度解析cmse-nonsecure-callABI 的参数与返回值寄存器约束【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rustE0798 是 rustc 针对 ARMv8-M TrustZone 安全扩展下cmse-nonsecure-call以及cmse-nonsecure-entryABI 施加的签名合法性校验错误当被调用方与调用方处于不同安全状态、只能通过寄存器而非栈传递数据时编译器要求函数指针的输入必须装进 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字段是VecSpan——所有导致溢出的参数都会被依次标记而不是只报第一个。触发 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放入 r0u64需要 8 字节对齐必须占用r2 与 r3r1 被填充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剥掉包装层再判断底层类型。对应的诊断结构体是CmseOutputStackSpilldiagnostics.rs其附注完整复述了这条规则the result must either be a (transparently wrapped) i64, u64 or f64, or be at most 4 bytes in size并在返回类型上标记this type doesnt fit in the available registers。泛型与impl Trait签名必须是完全具体化的第三条限制no generics can be used in the signature的触发面比字面意思更广源码中由两条路径覆盖泛型generic 类型参数布局计算对泛型类型返回LayoutError::TooGeneric此时should_emit_layout_errorcmse.rs会将其转发为CmseGeneric诊断diagnostics.rs消息为generics are not allowed in cmse-nonsecure-call signatures。因为该 ABI 本就要求参数/返回值按寄存器精确排布泛型意味着无法确定布局impl Traitopaque 类型对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.rsvalidate_cmse_abicmse.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),仅供参考