Rust的内嵌汇编(入门)

本文是简单记录一下我入门学到的内容,防止后面忘了。

平台限制

当前版本"1.69", 只能支持,x86、x86-64、ARM、AArch64、RISC-V这几种cpu架构的汇编代码。

语法

这是参考手册中写的一个ABNF的定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
format_string := STRING_LITERAL / RAW_STRING_LITERAL
dir_spec := "in" / "out" / "lateout" / "inout" / "inlateout"
reg_spec := <register class> / "\"" <explicit register> "\""
operand_expr := expr / "_" / expr "=>" expr / expr "=>" "_"
reg_operand := dir_spec "(" reg_spec ")" operand_expr
operand := reg_operand
clobber_abi := "clobber_abi(" <abi> *("," <abi>) [","] ")"
option := "pure" / "nomem" / "readonly" / "preserves_flags" / "noreturn" / "nostack" / "att_syntax" / "raw"
options := "options(" option *("," option) [","] ")"
asm := "asm!(" format_string *("," format_string) *("," [ident "="] operand) *("," clobber_abi) *("," options) [","] ")"
global_asm := "global_asm!(" format_string *("," format_string) *("," [ident "="] operand) *("," options) [","] ")"

看着比较繁琐,简而言之就是有如下格式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 每个部分都可以以逗号分隔,写若干个。
// 具体每个部分的写法,可以看上面的定义,也可以看后面的详细说明
asm!(
    汇编代码部分,
    // 原文的operand,因为实际就是reg_operand, 
    // 个人理解就是将寄存器和变量进行绑定或映射操作的。有错请指正。
    寄存器指定部分,
    clobber_abi部分,   // 这个clobber_abi 我不知道该如何翻译,就不翻译了。
    配置选项部分
);

global_asm!(
    汇编代码部分,
    寄存器指定部分,
    配置选项部分
);

作用域

asm!在函数中使用,可以集成到编译器生成的函数汇编代码中。但是,这些汇编代码需要遵守严格的规则,去避免未定义的行为。编译器可能会将asm!定义的汇编代码提取出为一个函数,然后以函数调用的方式使用汇编代码。

global_asm!1

With the global_asm! macro, the assembly code is emitted in a global scope, outside a function. This can be used to hand-write entire functions using assembly code, and generally provides much more freedom to use arbitrary registers and assembler directives.

汇编代码部分

这里就是使用字符串常量去写汇编代码。简而言之,这里的就是println!中的第一个参数的格式化字符串的用法类似。除了不支持“隐式命名参数2

隐式命名参数的形式是:

1
2
3
4
fn main(){
    let a = 123;
    println!("{a}");
}

原因很简单,asm!需要指定寄存器的,像上面的例子那样的方式,变量a使用的哪一个寄存器就是没有指定的。

下面是《rust by examples》中的例子(后续很多例子也来自它,不再赘述)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#![allow(unused)]
fn main() {
    use std::arch::asm;

    let i: u64 = 3;
    let o: u64;
    unsafe {
        asm!(
            "mov {0}, {1}",
            "add {0}, 5",
            out(reg) o,
            in(reg) i,
        );
    }
    assert_eq!(o, 8);
}

寄存器指定部分

这部分就是申请寄存器,并将一些表达式和寄存器进行绑定或映射。 编译器会根据申请的寄存器,进行一些寄存器的入栈保护等操作。

  • in(<reg>) <expr>
    • <reg>可以使用"register class"3或者寄存器名去指定寄存器
    • 在汇编代码开始前,指定的寄存器会保存着<expr>的值
    • 在汇编代码结束时,指定的寄存器要依旧保存着<expr>的值,(除非后续使用了lateout申请这个寄存器)
  • out(<reg>) <expr>
    • <reg>可以使用"register class"或者寄存器名去指定寄存器
    • 申请的寄存器在汇编代码开始时,其中的值是未定义的。
    • <expr>要求是一个可以未初始化的place expression,在汇编代码结束时,寄存器的值会被写入表达式中。
    • <expr>可以是一个_,这个意味着该寄存器的值将会在汇编代码结束的时候被丢弃
  • lateout(<reg>) <expr>
    • 类似out(<reg>) <expr>, 但是lateout可以重复指定in指定的寄存器
    • 应该在读取全部输入后,再向该寄存器写入。(不这么做好像rust也发现不了,不安全的代码需要注意安全啊)
  • inout(<reg>) <expr>
    • <reg>可以使用"register class"或者寄存器名去指定寄存器
    • 在汇编代码开始前,指定的寄存器会保存着<expr>的值
    • <expr>要求是一个可变的(mut修饰)已经初始化的place expression,在汇编代码结束时,寄存器的值会被写入表达式中。 简而言是就是inout的结合,“可变和已初始化”就是为了同时满足可以读取、可以写入的要求
  • inout(<reg>) <in expr> => <out expr>
    • 类似前一个inout的用法,除了
    • 寄存器初始值是<in expr>
    • <out expr>要求是一个可以未初始化的place expression
    • <expr>可以是一个_,这个意味着该寄存器的值将会在汇编代码结束的时候被丢弃
    • <in expr><out expr可能有不同的类型。 简而言之就是将in和out映射的表达式分开,减少了一些限制
  • inlateout(<reg>) <expr> / inlateout(<reg>) <in expr> => <out expr>
    • 类似inout,除了可以复用in申请的寄存器。(这个只会发生在ininlateout之间的初始值相等时)
    • inlateoutinout之间的区别,就类似lateoutout之间的区别。
    • 应该在读取全部输入后,再向该寄存器写入。
  • sym <path>
    • must refer to a fn or static.
    • A mangled symbol name referring to the item is substituted into the asm template string.
    • The substituted string does not include any modifiers (e.g. GOT, PLT, relocations, etc).
    • is allowed to point to a #[thread_local] static, in which case the asm code can combine the symbol with relocations (e.g. @plt, @TPOFF) to read from thread-local data. 不是很理解,也没找到例子,就抄原文内容放在这里好了。

这里相关还有很多的内容,详细见参rust参考手册

clobber_abi部分

clobber 就是在汇编代码中可能会破坏某些寄存器或内存的值。

为了防止这些破坏影响外部代码,我们需要告诉编译器让其提前将这些资源保护起来。我们可以手动的使用out("a1") _inout("a1") 0 => _等方式手动实现,也可以通过clobber_api自动实现。

clobber_abi就是rust预先定义了一些abi可能破坏的寄存器资源集合,可以简单的通过一个参数告诉编译器。而不需要一长串重复的操作。 clobber_abi原理就是为指定集合中没有使用的寄存器,添加lateout("...") _的隐式声明去告诉编译器进行寄存器状态的保护。

限制:Generic register class outputs are disallowed by the compiler when clobber_abi is used: all outputs must specify an explicit register. Explicit register outputs have precedence over the implicit clobbers inserted by clobber_abi: a clobber will only be inserted for a register if that register is not used as an output.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

#![allow(unused)]
fn main() {
    use std::arch::asm;

    extern "C" fn foo(arg: i32) -> i32 {
        println!("arg = {}", arg);
        arg * 2
    }

    fn call_foo(arg: i32) -> i32 {
        unsafe {
            let result;
            asm!(
                "call *{}",
                // Function pointer to call
                in(reg) foo,
                // 1st argument in rdi
                in("rdi") arg,
                // Return value in rax
                out("rax") result,
                // Mark all registers which are not preserved by the "C" calling
                // convention as clobbered.
                clobber_abi("C"),
            );
            result
        }
    }
}

配置选项部分

略,详见 https://doc.rust-lang.org/reference/inline-assembly.html#options

asm!需要遵守的规则

这里简单说一点:不能假设汇编代码只会在输出的binary文件中出现一次。编译器可能会有多个asm!拷贝。例如含有asm!块的函数在多个地方被内联。

影响:不能简单的使用lable在汇编代码中,我们需要使用GNU assembler numeric local labels

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

#![allow(unused)]
fn main() {
    use std::arch::asm;

    let mut a = 0;
    unsafe {
        asm!(
            "mov {0}, 10",
            "2:",
            "sub {0}, 1",
            "cmp {0}, 3",
            "jle 2f",
            "jmp 2b",
            "2:",
            "add {0}, 2",
            out(reg) a
        );
    }
    assert_eq!(a, 5);
}

规则内容太多,详细见: https://doc.rust-lang.org/reference/inline-assembly.html#rules-for-inline-assembly

总结

这篇文章讲的内容比较简陋和有些混乱吧。一方面想按照参考手册的格式和内容写,但是内容太多了,不想写了,就放了一堆连接;另一方面则是刚学,很多地方自己也是懵懵懂懂的。有待之后,再改改这篇文章吧。

参考资料

  1. 《rust reference》
  2. 《rust by example》

  1. global_asm!asm!具有很多相似的部分,但是对于两者不同的部分,我在文档中确实没有读明白。所以这里我就直接放上原文的内容了。本文主要讲的也是asm!,感觉global_asm!的内容在参考手册中几乎没有。(或者有,我没理解到?) ↩︎

  2. 这里原文是“implicit named arguments”,我不确定是否有翻译正确,但是直接原文又觉得太长了,怪怪的。对应的RFC #2795 ↩︎

  3. 这个就是rust定义的一些寄存器的集合,使用这些集合的名字,编译器会自动的在集合中选择一个寄存器使用。简化寄存器指定。 ↩︎

使用 Hugo 构建
主题 StackJimmy 设计