news 2026/9/11 13:30:45

rustc E0798 深度解析:`cmse-nonsecure-call` ABI 的参数与返回值寄存器约束

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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/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 的枚举定义中表述得非常直白:CmseNonSecureCallCmseNonSecureEntry被注释为"extremely constrained barely-C ABI for TrustZone"(面向 TrustZone 的、约束极其严格的准 C ABI),对应字符串分别为"cmse-nonsecure-call""cmse-nonsecure-entry"(见 extern_abi.rs)。

E0798 的具体内容归纳为三条硬性限制(原文见 E0798.md):

  1. 输入参数必须能放入 4 个可用的 32 位参数寄存器(即 r0–r3,总计 16 字节),且对齐(alignment)参与计算
  2. 返回值必须 ≤ 4 字节,或者是一个 8 字节的基础类型(i64u64f64);
  3. 签名中不允许出现泛型

注意:这些规则仅作用于函数指针类型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_outputis_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) )

三条结论由此得出:

  1. 尺寸 ≤ 4 字节的类型(u32f32、指针等)合法;
  2. 尺寸为 8 字节的类型只有三种直接合法:i64u64f64。8 字节的复合类型(如(u32, u32))不合法;
  3. **透明包装(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-callcmse-nonsecure-entry两个 ABI 的校验,但文档示例聚焦前者;后者多一层"C 变参(c-variadic)直接放行"的例外处理(见 cmse.rs)。

触发路径与诊断实现:校验发生在哪一步

E0798 的完整链路如下:

  1. 类型检查阶段,lower_fn_ty在将 HIR 中的函数类型降级为内部ty::PolyFnSig时调用cmse::validate_cmse_abi(...)(调用点见 mod.rs);
  2. 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,变参直接返回;
  3. 分别调用is_valid_cmse_inputsis_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 时按以下思路处理:

  1. 精简参数:将参数总量压到 16 字节以内,且始终把 8 字节对齐的大类型(u64f64等)安排在不会被填充浪费的位置,避免对齐 padding 挤占寄存器(参考原文档第二个示例的反例);
  2. 规整返回值:返回u32/f32/指针等 ≤ 4 字节类型,或i64/u64/f64(含#[repr(transparent)]包装),不要返回 8 字节的复合类型;
  3. 消除泛型:确保cmse-nonsecure-call函数指针与cmse-nonsecure-entry入口函数的签名完全具体化,不使用泛型参数与impl Trait返回类型;
  4. 确认目标平台:该 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 13:29:48

2026年长沙大桌火锅推荐,跑了6家门店对比锅底与食材

一、2026年长沙大桌火锅推荐为何备受食客关注&#xff1f;2026年&#xff0c;长沙大桌火锅推荐逐渐成为食客关注的焦点&#xff0c;不少人追问哪些门店值得一试。从大众点评资深用户的评价维度来看&#xff0c;一桌好吃的火锅&#xff0c;必须经得起锅底风味、食材新鲜度、用餐…

作者头像 李华
网站建设 2026/9/11 13:27:19

HAProxy高级功能实战:ACL、一致性哈希与动态限流

先说个真实感受&#xff1a;很多团队把 HAProxy 当“高级版的四层转发器”&#xff0c;配个roundrobin加后端 IP 列表就完事了&#xff0c;等到需要做灰度、按 URL 分流、动态限流、会话一致性时才发现&#xff0c;这工具能玩的花样远比想象中多。这篇文章就从我自己的使用经验…

作者头像 李华
网站建设 2026/9/11 13:26:02

STM32F103C8T6 手工Bootloader开发:向量重映射、Flash编程与安全跳转

简介&#xff1a;本资源是一套面向嵌入式开发初学者与进阶工程师的STM32F103C8T6专用Bootloader完整实现方案&#xff0c;聚焦固件安全启动、在线升级&#xff08;IAP&#xff09;及多通信接口烧录等核心需求。项目覆盖启动模式配置、Flash编程算法、UART/USB协议交互、CRC校验…

作者头像 李华
网站建设 2026/9/11 13:24:04

如何用 GraphRAG 的 prompt-tune 命令生成领域适配的索引提示词?

如何用 GraphRAG 的 prompt-tune 命令生成领域适配的索引提示词&#xff1f; 【免费下载链接】graphrag A modular graph-based Retrieval-Augmented Generation (RAG) system 项目地址: https://gitcode.com/GitHub_Trending/gr/graphrag GraphRAG 的索引流水线默认使用…

作者头像 李华
网站建设 2026/9/11 13:21:46

低功耗开发入行地图:安卓与嵌入式功耗优化核心知识梳理

做功耗这行时间长了&#xff0c;经常有刚毕业或者想转方向的朋友问我&#xff1a;“低功耗开发到底是干什么的&#xff1f;是不是就是让手机待机时间长一点&#xff1f;”说实话&#xff0c;这个理解不算错&#xff0c;但离真实的岗位需求差得有点远。功耗优化不是某一项具体技…

作者头像 李华