新的内联汇编语法在 nightly 版本中可用

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

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

Nightly Rust 长期以来都有一种“内联汇编”(asm!)的语法;但是,这种语法只是暴露了 LLVM 汇编结构的非常原始的版本,没有任何安全措施来帮助开发人员使用它。即使稍微弄错此语法的任何细节,也往往会产生内部编译器错误 (ICE),而不是您期望从 rustc 获得的友好错误消息。此语法也很容易出错,原因还在于:它看起来与 GCC 的内联汇编语法相似,但存在细微的差异(例如寄存器约束中的名称)。此外,此语法几乎没有希望在任何非 LLVM 后端上得到支持。由于所有这些限制,尽管 asm! 语法是最受欢迎的功能之一,但它极不可能从 nightly 版本升级到 stable Rust 版本。

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

  • 该提案最初是以 内部预 RFC 的形式开始的。
  • 内联汇编成为语言团队的第一个 项目组 之一,并在 项目组存储库 中迭代设计 RFC。
  • RFC 2873(仍在讨论中)提供了语法及其与 Rust 语言交互的规范。
  • 我们将现有的 asm! 重命名为 llvm_asm!,以便目前在 nightly 版本上使用内联汇编的人员可以继续使用现有的语法。(我们计划最终删除此语法,因为它具有脆弱的 ICE 易发特性,但在评估新语法时,我们希望提供旧语法以供比较和替代。)
  • PR 69171(同样由 Amanieu 完成)在 nightly 版本中实现了新的 asm! 语法。

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

#![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 系统调用约定所需的精确输入、输出和副作用。您还可以通过任意寄存器提供输入和输出,编译器将为您选择合适的寄存器。以下示例使用位操作指令来计算值中所有设置位的位号,并将它们存储在内存切片中

#![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 流上创建主题来讨论内联汇编。