每夜版中提供新的内联汇编语法

2020 年 6 月 8 日 · Josh Triplett 代表 语言团队 发布

在优化、操作系统或嵌入式开发,或其他低级编程中,有时您可能需要为您正在运行的处理器编写原生汇编代码。“内联汇编”提供了一种将一些汇编指令集成到 Rust 程序中的简单方法,将 Rust 表达式作为输入寄存器,并将输出直接写入 Rust 变量。我们在每夜版 Rust 中引入了一种新的内联汇编语法,并正在征求反馈;我们相信这种新语法未来有稳定化的可能。

每夜版 Rust 很早之前就有了“内联汇编” (asm!) 的语法;然而,这个语法只是暴露了 LLVM 汇编构造的非常原始的版本,没有任何保护措施来帮助开发者使用它。稍微用错一点细节就可能导致内部编译器错误(ICE),而不是您期望从 rustc 那样友好的错误消息。这种语法还容易出错,原因在于:它看起来类似于 GCC 的内联汇编语法,但存在细微差异(例如寄存器约束中的名称)。这种语法也几乎不可能在任何非 LLVM 后端上得到支持。由于所有这些限制,asm! 语法尽管是最受欢迎的功能之一,但极不可能从每夜版升级到稳定版 Rust。

为了改进 asm! 并使其惠及更多用户,Amanieu d'Antras 设计并实现了一种新的、更友好的 asm! 语法。这种语法从概念到编译器实现经历了漫长的过程。

  • 该提议最初是在 internals 上的一个 pre-RFC
  • 内联汇编成为了语言团队的首批项目组之一,并在项目组仓库中迭代设计了 RFC。
  • RFC 2873(仍在讨论中)提供了语法及其与 Rust 语言交互的规范。
  • 我们将现有的 asm! 重命名为 llvm_asm!,以便目前在每夜版上使用内联汇编的用户可以暂时继续使用现有语法。(我们计划最终移除此语法,因为它脆弱且容易导致 ICE,但在评估新语法时,我们希望保留旧语法以便比较和作为替代方案。)
  • PR 69171(同样由 Amanieu 提交)在每夜版中实现了新的 asm! 语法。

以下是使用新内联汇编语法的一个示例,用于在 x86-64 Linux 上使用直接的 write 系统调用打印消息到标准输出。

#![feature(asm)]

fn main() {
    let buf = "Hello from asm!\n";
    let ret: i32;
    unsafe {
        asm!(
            "syscall",
            in("rax") 1, // syscall number
            in("rdi") 1, // fd (stdout)
            in("rsi") buf.as_ptr(),
            in("rdx") buf.len(),
            out("rcx") _, // clobbered by syscalls
            out("r11") _, // clobbered by syscalls
            lateout("rax") ret,
        );
    }
    println!("write returned: {}", ret);
}

(您可以在playground 上尝试此示例。)

上面的示例指定了 Linux 系统调用约定所需的精确输入、输出和 clobber。您还可以通过任意寄存器提供输入和输出,编译器会为您选择合适的寄存器。以下示例使用位操作指令计算一个值中所有设置位(set bits)的位编号,并将它们存储在内存切片中。

#![feature(asm)]

fn main() {
    let mut bits = [0u8; 64];
    for value in 0..=1024u64 {
        let popcnt;
        unsafe {
            asm!(
                "popcnt {popcnt}, {v}",
                "2:",
                "blsi rax, {v}",
                "jz 1f",
                "xor {v}, rax",
                "tzcnt rax, rax",
                "stosb",
                "jmp 2b",
                "1:",
                v = inout(reg) value => _,
                popcnt = out(reg) popcnt,
                out("rax") _, // scratch
                inout("rdi") bits.as_mut_ptr() => _,
            );
        }
        println!("bits of {}: {:?}", value, &bits[0..popcnt]);
    }
}

(您可以在playground 上尝试此示例。请注意,此代码用于演示内联汇编,而不是演示任何特定算法的高效实现。)

请注意,valuepopcnt 的寄存器由编译器选择,而 bits.as_mut_ptr() 必须放到 rdi 寄存器中才能与 stosb 指令一起使用。

另外,请注意,在 x86 平台上,asm! 默认使用 Intel 语法;但是,您可以使用 option(att_syntax) 来使用 AT&T 语法。在将现有内联汇编代码翻译到新的 asm! 语法时,您可能会觉得这很有用。

有关新 asm! 语法的完整细节,请参阅RFC 2873。请尝试使用它(包括将现有内联汇编翻译到新语法),并使用标签 F-asm 通过Rust Issue Tracker 报告任何错误。您还可以在 Zulip 的 project-inline-asm 流中创建主题来讨论内联汇编。