Rust :裸函数 naked functions

原文

一、什么是"裸函数"(naked functions)

  • 裸函数用 #[unsafe(naked)] 标注,函数体只能是一个 naked_asm! 调用。
  • "裸"的含义是:编译器不会为它自动生成常规函数的"前奏/尾声"(prologue/epilogue)代码,也不会帮你处理参数传递或返回值;函数体完全由你写的汇编指令决定。
  • 这和普通的 asm! 不同:普通函数里即便用了 asm!,编译器仍会生成函数的栈帧、保存寄存器等;而 naked 函数则完全不加这些"包装"。

二、为什么需要它(和 global_asm! 相比有什么优势)

过去如果你要写"完全由汇编控制的函数",常见的做法是用 global_asm! 直接把一段全局汇编塞进目标文件。这样做有几个痛点:

  1. 符号/平台指令的样板代码繁琐且平台相关

    • 你得自己写 .section/.globl/.type/.size/.p2align 等指令来"声明一个函数",而且不同目标平台(ELF、Mach-O、COFF)写法不同,机械又容易错。
    • 裸函数会自动生成这些平台相关的指令,你只写核心汇编逻辑即可。
  2. 符号名与名称修饰(name mangling)问题

    • 用 global_asm! 时你往往把函数名硬编码成裸符号,不参与 Rust 的名称修饰。这会引起:

      • 跨平台兼容性差(比如 macOS x86_64 符号前缀有 _,Linux 没有)。
      • 需要让符号全局可见,易发生符号冲突。
    • 用裸函数时,函数名按 Rust 规则处理、参与名称修饰,减少这些问题。

  3. 泛型支持受限

    • global_asm! 定义的函数无法像正常 Rust 函数一样使用(尤其是 const generics 等)。裸函数因为是"正常函数签名 + 特殊宏体",可以更自然地结合一些泛型场景(受限于汇编可接受的操作数种类)。
  4. 统一的文档与安全注释位置

    • 裸函数只有一个定义点,你可以在它的注释里集中写明安全性约束(非常重要)。global_asm! 往往需要额外的 extern 声明和分散文档,更容易过时或不一致。

三、它解决的核心问题与使用场景

裸函数的典型使用场景都是"非常低层"的地方,你需要绝对控制指令序列与 ABI 交互,不希望编译器插手(如保存/恢复寄存器、栈帧布局等)。

常见场景:

  • 启动代码、引导程序、上下文切换

    • 比如在操作系统内核、引导加载器、裸机(embedded)环境,函数需要用特定寄存器传参/返回,不希望任何多余指令。
  • 异常/中断入口、陷入处理(trampoline)

    • 必须手动保存/恢复寄存器、切换栈等,常规函数前后序会干扰。
  • 编译器内建(compiler-builtins)、特定 ABI 粘合层

    • 一些平台或架构有非标准 ABI 的库函数(例如 ARM EABI 的 _*aeabi** 例程),需要"用你自己定义的调用约定"来衔接其他 C 例程或汇编。
  • 极致性能和确定性

    • 某些关键路径上希望精确到每条指令的控制。

四、为什么是"unsafe"

  • 裸函数跳过了编译器的 ABI 与栈帧安全网,意味着:

    • 你必须确保你写的汇编与声明的 ABI、函数签名完全一致(参数在哪些寄存器、谁来保存哪些寄存器、如何返回值等)。
    • 稍有不当就可能破坏栈、寄存器约定,导致未定义行为或崩溃。
  • 因此 #[unsafe(naked)] 明确标示"这是不安全接口",需要配套完整的 SAFETY 注释说明不变量与约定。

五、naked_asm! 是什么

  • 它是专门给裸函数用的汇编宏,介于 asm! 与 global_asm! 之间:

    • 写在函数体里(像 asm!),但只接受部分操作数类型(像 global_asm!)。
    • 编译器将裸函数"下沉"为全局汇编,从而保证函数体"只包含你写的指令",避免 LLVM 等后端偷偷插入额外指令,并且适配非 LLVM 后端。

六、和未来相关的两个扩展(实验中)

  • extern "custom"(feature: abi_custom)

    • 现实中很多裸函数并不真遵循 C 或 sysv64 之类标准 ABI,而是"自定义调用约定"。通过 extern "custom" 能把这种事实表达清楚。
    • 重要限制:这类函数不能被普通 Rust 调用表达式直接调用(编译器不知道怎么生成正确调用序列),只能通过内联汇编去"跳"过去。
  • cfg_asm(feature: cfg_asm)

    • 允许对汇编块内的"某几行"加 #[cfg(...)] 条件编译。便于按目标/特性开关精细裁剪汇编,而不是复制整段汇编。

七、一个简化的示例对比

  • 裸函数写法(更简洁、可移植):

    • #[unsafe(naked)]
      pub extern "sysv64" fn wrapping_add(a: u64, b: u64) -> u64 {
      core::arch::naked_asm!(
      "lea rax, [rdi + rsi]",
      "ret",
      );
      }
  • 如果用 global_asm!,你得写一堆 .section/.globl/.type/.size 等平台相关指令,还要处理符号名修饰和可见性问题。

八、我该不该用?

  • 如果你在写普通应用(Web 服务、CLI、桌面程序),几乎不需要。

  • 如果你在写:

    • OS 内核/引导、嵌入式启动代码/中断向量、
    • 需要完全掌控寄存器与栈的上下文切换、
    • 与非标准 ABI 的底层粘合层、
    • 或想在极少数路径上精确控制指令序
      那么裸函数会显著提升可读性、可维护性与可移植性,同时减少 global_asm! 的样板与坑点。

九、使用时的注意事项

  • 必写清晰的 SAFETY 注释:说明所遵循的 ABI、寄存器约定、被破坏/保存的寄存器、栈使用方式、返回值规则等。
  • 确保签名、ABI、实现一致。例如 extern "sysv64" 时,x86_64 的参数寄存器是 rdi、rsi、rdx、rcx、r8、r9,返回值在 rax(必要时 rdx)。
  • 谨慎处理被调用者保存(callee-saved)与调用者保存(caller-saved)寄存器。
  • 在嵌入式/无操作系统环境,留意栈指针初始化、中断屏蔽、内存栅栏等。

一句话总结:

  • 裸函数把"完全由汇编控制函数体"的需求变得更安全、更清晰、更可移植,替代过去繁琐且容易出错的 global_asm! 方案,主要服务于内核、嵌入式、编译器内建等极低层场景。如果你不写这类底层代码,基本用不到;但在需要它的地方,这是一个非常重要的可维护性和健壮性提升。Pomelo_刘金.
相关推荐
猫头虎6 小时前
Rust评测案例:Rust、Java、Python、Go、C++ 实现五大排序算法的执行时间效率比较(基于 OnlineGDB 平台)
java·开发语言·c++·python·golang·rust·排序算法
ftpeak6 小时前
Rust 嵌入式开发的经验之谈
开发语言·后端·rust
已黑化的小白17 小时前
Rust 的所有权系统,是一场对“共享即混乱”的编程革命
开发语言·后端·rust
John_Rey17 小时前
Rust类型系统奇技淫巧:幽灵类型(PhantomData)——理解编译器与类型安全
前端·安全·rust
rechol20 小时前
调试原理[简要描述]
嵌入式
阿源-21 小时前
C语言编译过程 & ELF文件加载过程解析
嵌入式·c/c++
John_Rey1 天前
Rust底层深度探究:自定义分配器(Allocators)——控制内存分配的精妙艺术
开发语言·后端·rust
勤奋的小小尘1 天前
第三篇: Rust 结构体、Trait 和方法详解
rust
isyuah1 天前
Rust Miko 框架系列(二):快速上手与基础示例
后端·rust