深入解读 Rust 编译器错误码 E0776:cmse_nonsecure_entry 函数的 ABI 约束与演进
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
E0776 是 rustc 错误码体系中与 Armv8-M TrustZone-M 安全扩展相关的历史错误码,用于约束 CMSE 非安全入口(non-secure entry)函数的调用约定。本文基于当前仓库中的 E0776 官方文档 展开,并结合编译器中 ABI 校验、LLVM 代码生成等源码实现,完整说明该错误码的产生背景、修复方式及其在现代 rustc 中的演进形态,帮助读者理解 CMSE 入口函数的参数传递限制与正确写法。
错误码现状:E0776 已不再由编译器发出
在深入探讨之前,必须先澄清一个重要事实:E0776 这个错误码在当前版本的 rustc 中已经不再被发出(no longer emitted)。
这一点在 error_codes/lib.rs 中有明确的注释记录:
// E0776, // Removed; `#[cmse_nonsecure_entry]` is now `extern "cmse-nonsecure-entry"`也就是说,旧的#[cmse_nonsecure_entry]属性写法已经被新的extern "cmse-nonsecure-entry"函数 ABI 语法所取代。错误码的删除并非因为功能被废弃,而是因为触发该错误的旧语法本身被重构了。rustc 错误码文档体系中保留 E0776 的说明,是为了让开发者(尤其是检索历史错误码的开发者)理解这条错误曾经的含义与修复方式。
错误码原始含义:入口函数需要 C ABI
原始错误信息
E0776 的原始错误信息为:
#[cmse_nonsecure_entry]functions require a C ABI(
#[cmse_nonsecure_entry]函数要求使用 C ABI)
在旧语法时代,#[cmse_nonsecure_entry]是一个函数属性,需要配合extern "C"使用。如果开发者忘记声明 C ABI,就会触发 E0776。
原始错误示例
E0776 文档中给出的错误示例代码如下:
#![feature(cmse_nonsecure_entry)] #[no_mangle] #[cmse_nonsecure_entry] pub fn entry_function(input: Vec<u32>) {}这段代码包含两个问题:
- 缺少 C ABI 声明:
#[cmse_nonsecure_entry]要求函数使用extern "C"调用约定; - 参数类型不当:
Vec<u32>是一个包含指针、长度、容量的复合类型,体积远超寄存器容量,无法通过寄存器传递。
原始修复方式
文档给出的修复方法是显式声明 C ABI:
#![feature(cmse_nonsecure_entry)] #[no_mangle] #[cmse_nonsecure_entry] pub extern "C" fn entry_function(input: Vec<u32>) {}需要说明的是,即便补上extern "C",Vec<u32>这类无法在寄存器中完整传递的类型仍会触发其他检查(详见下文源码分析),因此这只是针对 E0776 本身的修复示范。
现代写法:直接使用 cmse-nonsecure-entry ABI
E0776 被删除的根本原因,是 rustc 引入了专门的extern "cmse-nonsecure-entry"函数 ABI 语法,取代了原先的属性 + C ABI 组合。在 unstable-book 特性文档 中给出了当前的标准写法:
#![no_std] #![feature(cmse_nonsecure_entry)] #[no_mangle] pub extern "cmse-nonsecure-entry" fn entry_function(input: u32) -> u32 { input + 6 }关键点:
- 该特性名称为
cmse_nonsecure_entry,在 rustc_feature/src/unstable.rs 中登记为 unstable 特性,自 Rust 1.48.0 起引入,tracking issue 编号为 75835; - 函数仍需搭配
#[no_mangle]或类似导出手段,因为该 ABI 的用途就是生成可被链接器识别的安全网关入口符号; - ABI 语法
extern "cmse-nonsecure-entry"的定义位于 rustc_abi/src/extern_abi.rs,属于ExternAbi::CmseNonSecureEntry变体。
为什么参数必须通过寄存器传递:源码级原理
触发 E0776 的深层原因
E0776 之所以要求 C ABI,根因是 CMSE 非安全入口函数的参数和返回值不允许通过栈传递。这是因为安全世界与非安全世界之间共享栈,如果参数落在栈上,就可能泄露安全数据或成为攻击面。因此编译器强制要求所有参数都必须能装进寄存器。
在 rustc_ast_lowering/src/stability.rs 中,ExternAbi::CmseNonSecureEntry与CmseNonSecureCall一样被标记为需要 unstable feature gate:
ExternAbi::CmseNonSecureEntry => Err(UnstableAbi { abi, feature: sym::cmse_nonsecure_entry, explain: GateReason::Experimental, }),这解释了为什么示例代码开头必须写#![feature(cmse_nonsecure_entry)]。
ABI 校验实现:cmse.rs
rustc 在类型检查阶段的 HIR 类型降级(hir_ty_lowering)模块中实现了专门的 CMSE ABI 校验逻辑,文件位于 compiler/rustc_hir_analysis/src/hir_ty_lowering/cmse.rs。其中validate_cmse_abi函数负责检查两个核心约束:
1. 参数总量不能超过 4 个 32 位寄存器(16 字节)
is_valid_cmse_inputs函数(cmse.rs)逐个累加参数类型的内存布局大小,并做对齐处理:
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); }当累计大小超过 16 字节(即 4 个 32 位寄存器)时,编译器会发出CmseInputsStackSpill诊断,提示参数会溢出到栈上。这正解释了为何Vec<u32>无法作为入口函数参数——它的大小远超过寄存器容量。
2. 返回值必须能完整放入寄存器
is_valid_cmse_output_layout函数(cmse.rs)规定返回值要么不超过 4 字节,要么是恰好 8 字节且剥离开透明包装后为i64/u64/f64之一:
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) )此外,validate_cmse_abi还显式禁止两类情况:
- C 可变参数(c-variadic):
extern "cmse-nonsecure-entry"函数不能是可变参数函数(cmse.rs); - 返回值中的
impl Trait(opaque type):在返回类型中使用impl Trait会引发布局计算查询环,被显式禁止(cmse.rs)。
这些校验由 rustc_codegen_ssa/src/mir/block.rs 中的相关逻辑共同配合,确保从类型检查到 MIR 代码生成全程贯彻"参数不得使用非安全栈"这一安全约束。
LLVM 端的属性生成
在后端代码生成阶段,rustc_codegen_llvm/src/abi.rs 会为 CMSE 非安全入口函数附加 LLVM 函数属性:
CanonAbi::Arm(ArmCall::CCmseNonSecureEntry) => { func_attrs.push(llvm::CreateAttrString(cx.llcx, "cmse_nonsecure_entry")) }LLVM 收到该属性后会执行一系列平台相关的转换(具体在 LLVM 的 ARM 后端实现):为函数生成带__acle_se_前缀的导出符号、插入清除可能含安全信息的寄存器的代码、并以BXNS指令返回。这些行为在 unstable-book 特性文档 中有明确记载:
- 在函数上添加
__acle_se_前缀加标准函数名的特殊符号; - 约束参数数量以避免使用非安全栈;
- 函数返回前清除可能包含安全信息的寄存器;
- 使用
BXNS指令返回。
__acle_se_符号会被链接器用来生成安全网关 veneer(secure gateway veneer),这是 TrustZone-M 安全状态切换到非安全状态的必需机制。
相关错误码:E0775 与目标平台限制
CMSE 入口函数还有一条密切相关的错误码 E0775,两者同属 CMSE 特性家族,可以对照阅读。E0775 的文档位于 compiler/rustc_error_codes/src/error_codes/E0775.md,其含义为:
#[cmse_nonsecure_entry]is only valid for targets with the TrustZone-M extension.
即 CMSE 入口函数只能在具备 TrustZone-M 扩展的目标平台上使用。当前支持的目标平台包括:
thumbv8m.main-none-eabithumbv8m.main-none-eabihfthumbv8m.base-none-eabi
(对应 Armv8-M 架构轮廓,目标名中包含thumbv8m。)
E0776 与 E0775 的关系可以理解为:E0776 关注"调用约定是否符合要求"(是否使用 C ABI / 能否用寄存器传参),E0775 关注"目标平台是否支持"(是否具备 TrustZone-M 扩展)。
实战建议与总结
结合当前仓库源码,使用 CMSE 非安全入口函数时应遵循以下要点:
- 使用现代 ABI 语法:优先使用
extern "cmse-nonsecure-entry"而非旧的#[cmse_nonsecure_entry]属性 +extern "C"组合,这是 E0776 被移除后 rustc 推荐的写法; - 保持 unstable 特性启用:需要
#; - 控制参数与返回值体积:全部参数累计大小不得超过 16 字节(4 个 32 位寄存器),返回值只能是 ≤4 字节的类型或透明包装的 64 位标量(
i64/u64/f64),否则会触发参数/返回值栈溢出的编译错误——Vec<u32>这类堆分配类型绝不能用作出入口函数参数; - 选择正确的目标平台:目标必须是
thumbv8m系列(如thumbv8m.main-none-eabi),否则会触发 E0775; - 避免可变参数与
impl Trait:入口函数不允许 c-variadic 和 opaque 返回类型; - 配合导出符号使用:函数应使用
#[no_mangle],使编译器生成的__acle_se_前缀符号能正确导出,供链接器生成安全网关 veneer。
E0776 的消亡是 rustc 错误码体系随语言演进迭代的典型案例:一条错误码所约束的约束条件并未消失,而是随着更精确的 ABI 语法引入而转化为类型系统层面的直接检查。理解这条错误码的历史,也就理解了 CMSE 入口函数"参数必须走寄存器"这一核心安全设计原则。
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考