Rust 1.88 终于稳定了裸函数:写汇编不再需要那堆样板代码

Rust 1.88.0 稳定了 #[unsafe(naked)] 属性和 naked_asm! 宏,正式将裸函数(naked functions)带入稳定版本。

从最初的 RFC 提出算起,这个特性等了整整十年。

本文基于 Rust 官方博客 2025 年 7 月 3 日发布的《Stabilizing naked functions》整理撰写,作者为 Folkert de Vries。

原文地址


裸函数是什么

在普通的 Rust 函数中,编译器会在你写的代码前后悄悄插入一些处理逻辑------函数调用约定(calling convention)要求的寄存器保存和恢复、参数传递、返回值处理等等。这些都由编译器自动完成,通常你不需要关心。

但在操作系统内核、编译器内置函数(compiler-builtins)、嵌入式开发等场景里,有时你需要精确控制函数的每一条指令,不能让编译器擅自添加任何东西。这正是裸函数的用途。

顾名思义,"裸"的含义是:编译器不添加任何额外处理,函数体里只有你亲手写的汇编,多一条都没有。

用法示例:

rust 复制代码
/// SAFETY: 遵循 64 位 System-V ABI。
#[unsafe(naked)]
pub extern "sysv64" fn wrapping_add(a: u64, b: u64) -> u64 {
    // 等价于 a.wrapping_add(b)
    core::arch::naked_asm!(
        "lea rax, [rdi + rsi]",
        "ret"
    );
}

函数体内只有一个 naked_asm! 调用,没有其他 Rust 代码。这就是裸函数的全部形态。


为什么不直接用 global_asm!

在裸函数稳定之前,需要在汇编层面精确控制函数的代码通常用 global_asm! 来写。同样的 wrapping_add,用 global_asm! 实现是这样:

rust 复制代码
unsafe extern "sysv64" {
    safe fn wrapping_add(a: u64, b: u64) -> u64
}

core::arch::global_asm!(
    r#"
        .section .text.wrapping_add,"ax",@progbits
        .p2align 2
        .globl wrapping_add
        .type wrapping_add,@function

wrapping_add:
        lea rax, [rdi + rsi]
        ret

.Ltmp0:
        .size wrapping_add, .Ltmp0-wrapping_add
    "#
);

光是定义一个函数,就需要在汇编里手写一堆平台相关的指令(.section.p2align.globl.type.size......)。这些东西是机械性的样板代码,但在不同的目标平台和对象文件格式之间还各有差异,写错了就会出问题。

裸函数会自动生成这些指令,你只需要关注实际的汇编逻辑。

除此之外,global_asm! 还有几个额外的限制:

符号名不参与 Rust 的名称修饰(name mangling) :x86_64 macOS 的符号名会带 _ 前缀,Linux 不带,要做跨平台支持得手动处理这些差异。裸函数的符号名走 Rust 的正常名称修饰流程,不存在这个问题。

全局符号可见性global_asm! 定义的函数符号是全局可见的,可能与其他地方的同名符号冲突。裸函数通过名称修饰天然避免了这个问题。

不支持泛型global_asm! 里没有办法使用泛型,尤其是 const 泛型。裸函数支持泛型,这在某些场景下很有用。

文档与函数定义分离global_asm! 方案需要同时维护 extern 块的声明和 global_asm! 中的实现,安全注释和属性容易因为分散在两处而遗漏或失同步。裸函数把所有东西放在一个定义里。


十年的等待:这个特性的历史

裸函数最早的 RFC 提案可以追溯到 2015 年。

2020 年,随着 Rust 内联汇编语法经历了大规模重新设计,原有的 RFC 被 RFC 2972 取代。新的 RFC 将裸函数的函数体限定为单个汇编调用,并加了一些额外约束。

此后有两个关键的实现变化推动了这个特性走向稳定:

引入独立的 naked_asm! :最初的实现方案是在普通的 asm! 调用上加额外的 lint 检查。这种做法很难给出清晰的错误信息,文档也难以表达。naked_asm! 作为专用宏,行为更容易明确定义------它的语法是 asm!(因为在函数体内)和 global_asm!(只接受部分操作数类型)的混合体。

实现方式从依赖 LLVM 改为降级到全局汇编:早期的实现依赖 LLVM 的 naked 属性来做代码生成。问题是 LLVM 有时会在用户写的指令之外插入额外指令,而且 Rust 现在已经有了非 LLVM 的代码生成后端(比如 GCC 后端、Cranelift),它们需要自己实现一套 LLVM 的未规范化行为。现在的实现方案是把裸函数在编译器内部转换为全局汇编,所有代码生成后端都已经支持输出全局汇编,这同时也保证了最终输出的函数体里只有用户写的指令,一条不多。


一个需要特别注意的安全问题

naked 属性标记为 unsafe,这不是形式上的谨慎,而是有实际原因的。

裸函数的调用约定(ABI)、函数签名、以及汇编实现三者必须完全一致,完全由程序员自己保证。编译器不会插入任何辅助代码,一旦不一致,就可能产生难以调试的内存错误或未定义行为。

比如在上面的例子里,extern "sysv64" 说明这个函数遵循 64 位 System-V ABI,那汇编实现就必须按照这个 ABI 来处理参数和返回值------参数通过 rdirsi 传入,返回值放在 rax。写汇编的人需要自己确认这些约定是对的。

正确的安全注释在裸函数里是必要的,不是可选的。


接下来:两个正在开发的相关特性

裸函数稳定了,但 Rust 汇编生态的改进还在继续推进。有两个实验性特性值得关注。

extern "custom" 函数

裸函数通常搭配 extern "C" 调用约定使用,但在很多实际场景里,这其实是一个谎言------这些函数根本没有遵循任何 Rust 已知的 ABI,而是使用某种只属于该函数自身的自定义约定。

正在开发中的 abi_custom 特性引入了 extern "custom" 调用约定。一个来自 compiler-builtins 的真实例子:

rust 复制代码
#![feature(abi_custom)]

/// 使用 Arm 非标准 ABI 执行除法和取模运算。
#[unsafe(naked)]
pub unsafe extern "custom" fn __aeabi_idivmod() {
    core::arch::naked_asm!(
        "push {{r0, r1, r4, lr}}",
        "bl {trampoline}",
        "pop {{r1, r2}}",
        "muls r2, r2, r0",
        "subs r1, r1, r2",
        "pop {{r4, pc}}",
        trampoline = sym crate::arm::__aeabi_idiv,
    );
}

extern "custom" 有一个重要的语义:用这种约定声明的函数不能被 Rust 代码直接调用,只能通过内联汇编来执行。这是因为编译器不知道如何为一个未知 ABI 的函数生成调用代码,与其让错误静默地发生,不如在编译期报错。

汇编块里的 cfg 条件编译

cfg_asm 特性允许在汇编块的单行上使用 #[cfg(...)] 条件编译,而不需要为了一个条件分支就把整个汇编块复制一遍。

rust 复制代码
#![feature(cfg_asm)]

global_asm!(
    // ...
    #[cfg(feature = "set-sp")]
    "ldr r0, =_stack_start
     msr msp, r0",
    // ...
)

这个功能来自嵌入式开发的实际需求。以 cortex-m crate 为例,它现在不得不写一个自定义宏,每次用到条件编译时都把整块汇编复制一份------有了 cfg_asm,这种绕路就不再必要了。


小结

裸函数的稳定化,对于写操作系统、编译器工具链或嵌入式固件的 Rust 开发者来说是一个值得关注的改动。它不创造新的能力------global_asm! 一直都能做同样的事------但它显著降低了这类代码的书写复杂度,同时消除了一类平台相关的手工处理。

等了十年,从 RFC 到稳定版本,有时候事情就是需要这么久才能做对。


相关推荐
武子康2 小时前
大数据-271 Spark MLib-基础线性回归详解:从原理到损失优化实战
大数据·后端·spark
Postkarte不想说话2 小时前
LangChain使用入门
后端
xyyaihxl2 小时前
将 vue3 项目打包后部署在 springboot 项目运行
java·spring boot·后端
chenxu98b2 小时前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
sunwenjian8862 小时前
跨域问题解释及前后端解决方案(SpringBoot)
spring boot·后端·okhttp
神奇小汤圆2 小时前
快手一面:你简历做过 RAG 项目,那 RAG 主要用来解决什么问题?
后端
黑牛儿2 小时前
面试高频问题:从浏览器请求到PHP响应:完整流程拆解
android·后端·面试·php
BOOM朝朝朝2 小时前
envoy-gateway 解析
后端
搬搬砖得了2 小时前
Spring Boot Bean 生命周期与作用域:从单例到原型,完整剖析
后端·架构