Rust的各种花式汇编操作

Rust的各种花式汇编操作

  • 使用nightly rust的asm!宏
    • assembly template
    • 约束
      • 输出约束
      • 输入约束
    • Clobber约束
    • options
    • 更多例子
      • 操作MSR寄存器
      • 操作CR0寄存器
      • 操作RFLAGS寄存器
      • 修改CS寄存器
  • 在stable rust中嵌入汇编代码
    • 使用静态链接来嵌入汇编代码
    • 汇编函数的参数传递
    • 汇编函数的返回值
    • 向汇编函数传递指针
    • 向汇编函数传递数组
    • 向汇编函数传递结构体
      • 使用T作为参数
        • 使用&T作为参数

使用nightly rust的asm!宏

Rust的内联汇编基础语法如下(需要启用#!(feature(asm)))


asm!(
	assembly template 
	: 输出操作数
	: 输入操作数
	: Clobber
	: 选项
);

assembly template

assembly template是唯一需要的参数并且必须是原始字符串例如asm!("nop"),该指令不需要任何参数,因此省略了

因为Rust内联汇编还处于Unstable,因此我们需要使用#![feature(asm)]放在lib.rs文件对开头部分或main.rs中,例如

// in src/main.rs
#![feature(asm)]

fn main(){

}

使用时需要添加unsafe块或函数需要添加unsafe关键字,例如

pub unsafe fn nop(){
	asm!(
	"xor %eax, %eax"
	:
	:
	: "{eax}"
	:
	);
}
或者
pub fn nop(){
	unsafe{
		asm!(
		"xor %eax, %eax"
		:
		:
		: "{eax}"
		:
		);
	}
}


在调用unsafe函数时需要使用unsafe块

fn main(){
	unsafe{
		nop();
	}
}

如果一个函数只有一个内联汇编操作的话,建议将该函数声明为unsafe的,为了提高执行效率可以在函数上添加#[inline]宏(与C语言的#inline宏类似),这在编译时起到了优化的效果

#[inline]
pub unsafe fn xor(){
	// 有空格也没关系
	asm!("xor %eax, %eax" ::: "{eax}");
}

在调用该函数时我们可以添加#[cfg(target_arch = "x86_64")]来指定要编译的目标系统的架构,

模板字符串支持使用$后跟一个数字的参数替换例如$0,以指示由约束字符串指定的给定寄存器/内存位置的替换。${NUM:MODIFIER}也可以使用,其中MODIFIER是如何打印操作数的特定于目标的注释

字符 可 以 在 模 板 中 使 用 ‘ 可以在模板中使用` 使$`。要在输出中包含其他特殊字符,可以使用通常的“\XX”转义符,就像在其他字符串中一样。

约束

约束列表是逗号分隔的字符串,每个元素包含一个或多个约束代码,例如:“约束1”(表达式1),“约束2”(表达式2)...

对于约束列表中的每个元素,将选择一个适当的寄存器或内存操作数,并且将对$0列表中的第一个约束,$1第二个等将使其可用于组件模板字符串扩展。

输出约束

输出约束由“=”前缀(例如“=r”)指定。这表示程序集将写入此操作数,然后操作数将作为asm表达式的返回值提供。输出约束不会消耗调用指令中的参数。

LLVM输出约束的原文如下

通常,在读取所有输入之前,预计没有输出位置被汇编表达式写入。因此,LLVM可以将相同的寄存器分配给输出和输入。如果这不安全(例如,如果程序集包含两条指令,其中第一条写入一个输出,第二条读取输入并写入第二条输出),则必须使用“&”修饰符(例如“=&r”)来指定输出是“早期破坏”输出。将输出标记为“early-clobber”可确保LLVM不会对任何输入(除了与此输出关联的输入)使用相同的寄存器。

例如:
将cs寄存器的值移动到ax变量中

let ax: u16;
asm!(
	"movw %cs, %ax"
	: "={ax}"(ax)
	:
	:
);

输入约束

输入约束没有前缀 只是约束代码。每个输入约束将从调用指令中消耗一个参数。asm不允许写入任何输入寄存器或存储单元(除非该输入连接到输出)。还要注意,如果LLVM可以确定它们必然都包含相同的值,则可以将多个输入全部分配给相同的寄存器。

通过提供一个整数作为约束字符串,输入约束可以将它们自己绑定到输出约束,而不是提供约束代码。被绑定的输入仍然会从调用指令中消耗一个参数,并且按照通常的方式在asm模板编号中占据一个位置

它们将被简单地限制为始终使用与其绑定的输出相同的寄存器。例如,一个约束字符串“=r,0”表示为输出分配一个寄存器,并将该寄存器用作输入(它是第0个约束)

例如,将0x23移动到ss寄存器

asm!(
	"movw $0, %ss"
	:
	: "r"(0x23)
	: "memory"
);

指定寄存器名,可以使用多个参数

asm!("outb %al,%dx"
	: 
	:"{dx}"(0x21),"{al}"(0x21)
	:
);

所有目标通常都支持一些约束代码:

约束 解释
r 目标通用寄存器类中的寄存器
m 存储器地址操作数。它支持哪些寻址模式,典型的例子是寄存器,寄存器+寄存器偏移量,或寄存器+直接偏移量(某些目标特定的大小)
i 一个整数常量(目标特定宽度)。允许简单的即时或可重定位的值
n 一个整数常量 – 不包括可重定位值
s 一个整数常量,但只允许重定位值
X 允许任何类型的操作数,不受任何限制。通常用于为asm分支或call传递标签
{register-name} 需要完整的指定物理寄存器

Clobber约束

clobber不会消耗输入操作数,也不会输出操作数。

一些指令修改的寄存器可能保存有不同的值,所以我们使用覆盖列表来告诉编译器不要假设任何装载在这些寄存器的值是有效的

“memory”表示程序写入任意未声明的内存位置 不仅是由声明的间接输出指向的内存。

请注意,输出约束中存在的clobbering命名寄存器是不合法的。

约束代码可以是单个字母(例如“r”),“^”字符后跟两个字母(例如“^wc”)或“{”寄存器名称“ }”(例如“{eax}”)。

通常选择单字母和双字母约束代码与GCC的约束代码相同

一些指令修改的寄存器可能保存有不同的值,所以我们使用覆盖列表来告诉编译器不要假设任何装载在这些寄存器的值是有效的

options

最后一部分,options是 Rust 特有的。格式是逗号分隔的基本字符串(也就是说,:“volatile”, “intel”, “alignstack”)。它被用来指定关于内联汇编的额外信息:

目前有效的选项有:

  • volatile - 相当于 gcc/clang 中的__asm__ __volatile__ (...)
  • alignstack - 特定的指令需要栈按特定方式对齐(比如,SSE)并且指定这个告诉编译器插入通常的栈对齐代码
  • intel - 使用 intel 语法而不是默认的 AT&T 语法

例如使用Intel语法编写内联汇编

 asm!(
	 "mov eax, 2" 
	 : "={eax}"(result) 
	 : 
	 : 
	 : "intel"
 );

更多例子

操作MSR寄存器

MSR寄存器的写入操作

pub unsafe fn wrmsr(msr: u32, data: u64) {
    let low = data as u32; // 写入时需要将64位数据分解为2个32位数据
    let high = (data >> 32) as u32;
    asm!("wrmsr"
        :
        : "{ecx}"(msr),"{eax}"(low),"{edx}"(high)
        : "memory"
        : "volatile"
        )
}

MSR寄存器的读取操作

pub fn rdmsr(msr: u32) -> u64 {
    let (mut high, mut low) = (0_u32, 0_32); // 读取时需要将读到2个32位数据合并为64位数据
    unsafe{
        asm!("rdmsr"
        : "={eax}"(low),"={edx}"(high)
        : "{ecx}"(msr)
        : "memory"
        : "volatile"
        );
    }
    ((high as u64) << 32) | (low as u64)
}

操作CR0寄存器

写入CR0寄存器

pub unsafe fn write_cr0(value: u64) {
    // $0表示传递的value值
    asm!("mov $0, %cr0" 
        :
        :"r"(value)
        :"memory"
    )
}

读取CR0寄存器

pub unsafe fn read_cr0() -> u64 {
    let mut value: u64 = 0;
    asm!("mov %cr0, $0" 
        :"=r"(value)
    );
    value
}

操作RFLAGS寄存器

读取RFLAGS寄存器

pub unsafe fn read_rflags() -> u64 {
    let mut r: u64 = 0;
    // 多个汇编语句以;分割
    asm!("pushfq;  
          popq $0" 
        : "=r"(r) 
        :
        : "memory"
    );
    r
}

写入RFLAGS寄存器

pub unsafe fn write_raw(val: u64) {
    asm!("pushq $0; 
          popfq" 
        :
        : "r"(val) 
        : "memory" "flags"
    );
}

修改CS寄存器

CS寄存器表示当前执行的代码段,在重新加载GDT后需要重新设置CS段

pub unsafe fn set_cs(selector: u16) {
    // 把新的选择子压到栈中,并且使用lretq(远返回指令)重新加载cs寄存器并在1:处继续
    #[inline(always)]
    unsafe fn inner(selector: u16) {
        asm!(
            "pushq $0;
            leaq 1f(%rip), %rax;
            pushq %rax;
            lretq; 
            1:"
            :
            : "ri"(u64::from(selector)) // r表示目标通用寄存器类中的寄存器 i表示一个整数常量
            : "rax" "memory" // 声明该内嵌汇编会修改rax寄存器
        );
    }
    inner(selector);
}

在stable rust中嵌入汇编代码

因为asm!宏暂时只能在nightly rust中使用,如果想在stable rust中使用怎么办呢?(在看x86_64 crate中看到了他的用法)

使用静态链接来嵌入汇编代码

我们可以通过静态链接的方式来完成,步骤如下
首先建立一个项目cargo new call_test
在src文件中创建asm mod
结构如下

src/
├── asm
│   ├── asm.s
│   └── mod.rs
├── lib.rs
└── main.rs

其中asm.s就是我们要编写的汇编代码,其内容如下

.text ; 我们编写的代码在.text节中
.code64 ; 使用的是64位汇编代码

.global nop_func ; 需要导出的函数名
.p2align 4 ; .p2align 4 意思为在16字节边界上对齐 具体定义可参考 https://sourceware.org/binutils/docs/as/P2align.html#P2align
nop_func:
    nop ; 不做任何操作,空指令
    retq ; 函数返回

随后在src/asm/mod.rs中定义函数签名

#[link(name = "test_asm", kind = "static")] // 定义链接名称,使用的是静态链接方式
extern "C" {
    #[cfg(link_name = "nop_func",target_env = "gnu")] 
    // link_name必须与asm.s文件中.global后导出名称一致,
    // target_env = "gnu",target_env表示使用gun环境如果不添加则会出现 
    // unresolved import `asm_rust::asm::nop_func`
    // 这段代码只能在linux中使用
    pub fn nop_func();
}

最后在main函数调用函数

extern crate call_test;

use call_test::asm::{test_add,nop_func};

fn main() {
    unsafe{nop_func()};
}

编写完毕后如果使用cargo run来运行代码会发生错误,原因是我们没有编译我们自己写的汇编代码,我们需要做一些额外的操作

编译脚本
一些包需要编译第三方非Rust 代码,例如 C 库。其他的包需要链接到 C 库,cargo提供了build配置选项来完成这些功能
call_test/文件夹中创建build.rs(不是在call_test/src目录哦),至于build.rs文件的名字可以随意更改,最后在Cargo.toml中添加一下内容

[package]
name = "call_test"
version = "0.1.0"
authors = ["snake "]
edition = "2018"
build="build.rs" # 这里定义刚才创建的编译脚本

然后我们也需要使用cc crate来帮助编译

[build-dependencies]
cc = "1.0"

我们需要找到src/asm文件中的所有以.s结尾的文件(汇编文件),然后进行编译

// in call_test/build.rs
fn main(){
    use std::ffi::OsString;
    use std::fs;
    use cc::Build;
    // 在src/asm文件夹中寻找所有以.s结尾的文件
    let entry = fs::read_dir("src/asm").unwrap().filter_map(|f|{
        f.ok().and_then(|e|{
            let path = e.path();
            match path.extension(){
                Some(ext) if ext.eq(&OsString::from("s")) => Some(path),
                _ => None
            }
        })
    }).collect::>();    
    
    // 编译寻找到的.s文件
    Build::new()
        .no_default_flags(true) // 不使用默认的编译参数
        .files(&entry) // 传递寻找的汇编文件
        .pic(true) // 配置编译器是否将发出调试信息,默认为false
        .static_flag(true) // 设置-static 编译参数
        .shared_flag(false) // 不设置-shared 编译参数
        .compile("test_asm"); // 指定编译的名称test_asm,该名称必须要与#[link(name = "test_asm", kind = "static")]中的name一致
}

最后我们便可以使用cargo run来运行了

汇编函数的参数传递

首先我们在main.rs中编写一个add函数

//in main.rs
fn main() {
    let s = add(5,4);
}

pub fn add(a:i32,b:i32) -> i32{
    return a+b;
}

然后我们在main函数中调用add函数,这段代码很简单,然后我们在linux中使用cargo build编译后使用objdump命令反汇编编译好的代码

$ objdump -d target/debug/call_test > call_test.s 

然后我们在call_test.s文件中找到含有main字段的节,结果如下(编译名称可能稍有不同)

0000000000003e30 <_ZN8asm_rust4main17h0563b750d434c142E>:
    3e30:	50                   	push   %rax  // 在调用前会保存rax的值
    3e31:	bf 05 00 00 00       	mov    $0x5,%edi // 这个是我们调用的函数add(5,4); 可以看到函数的第一个参数会保存在%edi寄存器中
    3e36:	be 04 00 00 00       	mov    $0x4,%esi // 第二个参数会保存在esi寄存器中
    3e3b:	e8 10 00 00 00       	callq  3e50 <_ZN8asm_rust3add17h0d956a7707ae0cfaE> // 随后调用add函数
    3e40:	89 44 24 04          	mov    %eax,0x4(%rsp) // 将结果移到eax寄存器中 let s = add(5,4);
    3e44:	58                   	pop    %rax
    3e45:	c3                   	retq   
    3e46:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
    3e4d:	00 00 00 

add函数

add函数反汇编后结果如下

0000000000003e50 <_ZN8asm_rust3add17h0d956a7707ae0cfaE>:
    3e50:	48 83 ec 18          	sub    $0x18,%rsp // 分配所需要的使用的栈空间
    3e54:	89 7c 24 10          	mov    %edi,0x10(%rsp) // 将传递的第一个参数(0x5)压入栈中
    3e58:	89 74 24 14          	mov    %esi,0x14(%rsp) // 将传递的第二个参数(0x4)压入栈中
    3e5c:	8b 44 24 10          	mov    0x10(%rsp),%eax 
    3e60:	03 44 24 14          	add    0x14(%rsp),%eax // 将2个参数相加
    3e64:	0f 90 c1             	seto   %cl  // seto指令意思为 set if overflow 如果设置了溢出标志,则将操作数中的字节设置为1
    3e67:	f6 c1 01             	test   $0x1,%cl // 判断是否溢出
    3e6a:	89 44 24 0c          	mov    %eax,0xc(%rsp) // 将相加的结果压入栈中,为panic信息做参数使用
    3e6e:	75 09                	jne    3e79 <_ZN8asm_rust3add17h0d956a7707ae0cfaE+0x29> // 判断是否产生溢出,如果溢出则调用rust的panic
    3e70:	8b 44 24 0c          	mov    0xc(%rsp),%eax // 表示没有panic,恢复原值
    3e74:	48 83 c4 18          	add    $0x18,%rsp // 清空栈空间
    3e78:	c3                   	retq   // rax寄存器 作为函数调用的返回值
    // 剩下为加法溢出后panic的操作
    3e79:	48 8d 3d d0 1a 02 00 	lea    0x21ad0(%rip),%rdi        # 25950 
    3e80:	48 8d 15 f1 c7 22 00 	lea    0x22c7f1(%rip),%rdx        # 230678 <__init_array_end+0x8>
    3e87:	48 8d 05 92 c1 01 00 	lea    0x1c192(%rip),%rax        # 20020 <_ZN4core9panicking5panic17he6d6b86858b8480dE>
    3e8e:	be 1c 00 00 00       	mov    $0x1c,%esi
    3e93:	ff d0                	callq  *%rax
    3e95:	0f 0b                	ud2    
    3e97:	66 0f 1f 84 00 00 00 	nopw   0x0(%rax,%rax,1)
    3e9e:	00 00

以上我们通过简单的add函数分析了rust调用时所使用的寄存器,现在我们只测试2个参数分别通过RDI,RSI寄存器传递,RAX寄存器作为返回值,那么多个参数会使用那些寄存器呢
我们将add函数改为多个参数

fn main() {
    let s = add(1,2,3,4,5,6,7,8,9);
}

pub fn add(a:i32,b:i32,c:i32,d:i32,e:i32,f:i32,g:i32,h:i32,i:i32) -> i32{
    return a+b;
}

通过objdump反汇编后结果如下

0000000000003e30 <_ZN8asm_rust4main17h0563b750d434c142E>:
    3e30:	48 83 ec 28          	sub    $0x28,%rsp 
    3e34:	bf 01 00 00 00       	mov    $0x1,%edi // 第一个参数0x1
    3e39:	be 02 00 00 00       	mov    $0x2,%esi // 第二个参数0x2
    3e3e:	ba 03 00 00 00       	mov    $0x3,%edx // 第三个参数0x3
    3e43:	b9 04 00 00 00       	mov    $0x4,%ecx // 第四个参数0x4
    3e48:	41 b8 05 00 00 00    	mov    $0x5,%r8d // 第五个参数0x5
    3e4e:	41 b9 06 00 00 00    	mov    $0x6,%r9d // 第六个参数0x6
    3e54:	c7 04 24 07 00 00 00 	movl   $0x7,(%rsp) // 第七个参数0x7改为栈保存
    3e5b:	c7 44 24 08 08 00 00 	movl   $0x8,0x8(%rsp) // 第八个参数0x8改为栈保存,这里的偏移是8字节
    3e62:	00 
    3e63:	c7 44 24 10 09 00 00 	movl   $0x9,0x10(%rsp) // 第九个参数0x9改为栈保存
    3e6a:	00 
    3e6b:	e8 10 00 00 00       	callq  3e80 <_ZN8asm_rust3add17h10903d3f30e2f65dE>
    3e70:	89 44 24 24          	mov    %eax,0x24(%rsp)
    3e74:	48 83 c4 28          	add    $0x28,%rsp
    3e78:	c3                   	retq   
    3e79:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

add函数反汇编结果如下

0000000000003e80 <_ZN8asm_rust3add17h10903d3f30e2f65dE>:
    3e80:	53                   	push   %rbx // 因为会改变rbx寄存器的值,因此需要保存在栈中
    3e81:	48 83 ec 20          	sub    $0x20,%rsp // 分配所要使用的栈空间
    3e85:	8b 44 24 40          	mov    0x40(%rsp),%eax // 第七个参数
    3e89:	44 8b 54 24 38       	mov    0x38(%rsp),%r10d // 第八个参数
    3e8e:	44 8b 5c 24 30       	mov    0x30(%rsp),%r11d // 第九个参数
    3e93:	89 7c 24 08          	mov    %edi,0x8(%rsp) // 第一个参数
    3e97:	89 74 24 0c          	mov    %esi,0xc(%rsp) // 第二个参数
    3e9b:	89 54 24 10          	mov    %edx,0x10(%rsp) // 第三个参数
    3e9f:	89 4c 24 14          	mov    %ecx,0x14(%rsp) // 第四个参数
    3ea3:	44 89 44 24 18       	mov    %r8d,0x18(%rsp) // 第五个参数
    3ea8:	44 89 4c 24 1c       	mov    %r9d,0x1c(%rsp) // 第六个参数
    3ead:	8b 4c 24 08          	mov    0x8(%rsp),%ecx 
    3eb1:	03 4c 24 0c          	add    0xc(%rsp),%ecx // 第一个参数与第二个参数相加
    3eb5:	0f 90 c3             	seto   %bl // 如果设置了溢出标志,则将操作数中的字节设置为1
    3eb8:	f6 c3 01             	test   $0x1,%bl // 检查是否溢出
    3ebb:	89 4c 24 04          	mov    %ecx,0x4(%rsp) // 将相加的结果压入栈中,为panic信息做参数使用
    3ebf:	75 0a                	jne    3ecb <_ZN8asm_rust3add17h10903d3f30e2f65dE+0x4b> // 判断是否产生溢出,如果溢出则调用rust的panic
    3ec1:	8b 44 24 04          	mov    0x4(%rsp),%eax // 将返回值传递给eax寄存器中
    3ec5:	48 83 c4 20          	add    $0x20,%rsp // 恢复栈空间
    3ec9:	5b                   	pop    %rbx // 恢复保存的rbx寄存器中的值
    3eca:	c3                   	retq   // 函数返回
    3ecb:	48 8d 3d ce 1a 02 00 	lea    0x21ace(%rip),%rdi        # 259a0 
    3ed2:	48 8d 15 9f c7 22 00 	lea    0x22c79f(%rip),%rdx        # 230678 <__init_array_end+0x8>
    3ed9:	48 8d 05 90 c1 01 00 	lea    0x1c190(%rip),%rax        # 20070 <_ZN4core9panicking5panic17he6d6b86858b8480dE>
    3ee0:	be 1c 00 00 00       	mov    $0x1c,%esi
    3ee5:	ff d0                	callq  *%rax
    3ee7:	0f 0b                	ud2    
    3ee9:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

在add函数中我们传递了9个参数,可以发现前6个参数(包含第6个)使用的是RDI,RSI,RDX,RCX,R8,R9等寄存器传递,剩余的参数则使用栈传递

那么使用release模式生成的是否一致呢?
我们使用cargo build --release来编译,不过编译后代码会产生很大的改变,我们可以使用正则表达式来寻找call_test.*add结果如下

// fn main
0000000000003e30 <_ZN8asm_rust4main17h0563b750d434c142E>:
    3e30:	48 83 ec 28          	sub    $0x28,%rsp
    3e34:	bf 01 00 00 00       	mov    $0x1,%edi
    3e39:	be 02 00 00 00       	mov    $0x2,%esi
    3e3e:	ba 03 00 00 00       	mov    $0x3,%edx
    3e43:	b9 04 00 00 00       	mov    $0x4,%ecx
    3e48:	41 b8 05 00 00 00    	mov    $0x5,%r8d
    3e4e:	41 b9 06 00 00 00    	mov    $0x6,%r9d
    3e54:	c7 04 24 07 00 00 00 	movl   $0x7,(%rsp)
    3e5b:	c7 44 24 08 08 00 00 	movl   $0x8,0x8(%rsp)
    3e62:	00 
    3e63:	c7 44 24 10 09 00 00 	movl   $0x9,0x10(%rsp)
    3e6a:	00 
    3e6b:	e8 10 00 00 00       	callq  3e80 <_ZN8asm_rust3add17h10903d3f30e2f65dE>
    3e70:	89 44 24 24          	mov    %eax,0x24(%rsp)
    3e74:	48 83 c4 28          	add    $0x28,%rsp
    3e78:	c3                   	retq   
    3e79:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

// fn add
0000000000003e80 <_ZN8asm_rust3add17h10903d3f30e2f65dE>:
    3e80:	53                   	push   %rbx
    3e81:	48 83 ec 20          	sub    $0x20,%rsp
    3e85:	8b 44 24 40          	mov    0x40(%rsp),%eax
    3e89:	44 8b 54 24 38       	mov    0x38(%rsp),%r10d
    3e8e:	44 8b 5c 24 30       	mov    0x30(%rsp),%r11d
    3e93:	89 7c 24 08          	mov    %edi,0x8(%rsp)
    3e97:	89 74 24 0c          	mov    %esi,0xc(%rsp)
    3e9b:	89 54 24 10          	mov    %edx,0x10(%rsp)
    3e9f:	89 4c 24 14          	mov    %ecx,0x14(%rsp)
    3ea3:	44 89 44 24 18       	mov    %r8d,0x18(%rsp)
    3ea8:	44 89 4c 24 1c       	mov    %r9d,0x1c(%rsp)
    3ead:	8b 4c 24 08          	mov    0x8(%rsp),%ecx
    3eb1:	03 4c 24 0c          	add    0xc(%rsp),%ecx
    3eb5:	0f 90 c3             	seto   %bl
    3eb8:	f6 c3 01             	test   $0x1,%bl
    3ebb:	89 4c 24 04          	mov    %ecx,0x4(%rsp)
    3ebf:	75 0a                	jne    3ecb <_ZN8asm_rust3add17h10903d3f30e2f65dE+0x4b>
    3ec1:	8b 44 24 04          	mov    0x4(%rsp),%eax
    3ec5:	48 83 c4 20          	add    $0x20,%rsp
    3ec9:	5b                   	pop    %rbx
    3eca:	c3                   	retq   
    3ecb:	48 8d 3d ce 1a 02 00 	lea    0x21ace(%rip),%rdi        # 259a0 
    3ed2:	48 8d 15 9f c7 22 00 	lea    0x22c79f(%rip),%rdx        # 230678 <__init_array_end+0x8>
    3ed9:	48 8d 05 90 c1 01 00 	lea    0x1c190(%rip),%rax        # 20070 <_ZN4core9panicking5panic17he6d6b86858b8480dE>
    3ee0:	be 1c 00 00 00       	mov    $0x1c,%esi
    3ee5:	ff d0                	callq  *%rax
    3ee7:	0f 0b                	ud2    
    3ee9:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

可以看到release编译后对于add函数的代码并没有改变。

了解了Rust函数的调用约定后,就知道了怎样使用汇编进行传递参数

在asm.s中添加一个新的函数test_add

// in src/asm/asm.s
.global test_add
.p2align 4
test_add:
    mov %edi,%eax
    add %esi,%eax
    retq

然后我们在rust中定义函数签名

#[link(name = "test_asm", kind = "static")]
extern "C" {
    #[cfg(link_name="test_add")]
    pub fn test_add(port:i32) -> i32;
}

然后在main函数调用刚才定义函数

extern crate call_test;

use call_test::asm::test_add;

fn main() {
    let res = unsafe{test_add(1,2)};
    println!("call asm function result is: {}",res);
}

使用cargo run最后输出的结果为如下

call asm function result is: 3

汇编函数的返回值

我们知道如果函数只返回一个参数时使用rax寄存器,那么返回多个参数时会怎么传递呢,rust中可以使用元组返回多个值
我们定义一个res_test函数

fn main() {
    let s:(i32,i32) = res_test();
}

pub fn res_test() -> (i32,i32){
    (1,2)
}

通过objdump后反汇编结果如下

0000000000003e30 <_ZN8asm_rust4main17hacd67c1ea264c8ceE>:
    3e30:	50                   	push   %rax
    3e31:	e8 0a 00 00 00       	callq  3e40 <_ZN8asm_rust8res_test17hfbb14daa75e009ddE>
    3e36:	89 54 24 04          	mov    %edx,0x4(%rsp) ; 可以看到eax,edx寄存器保存的返回结果
    3e3a:	89 04 24             	mov    %eax,(%rsp)
    3e3d:	58                   	pop    %rax
    3e3e:	c3                   	retq   
    3e3f:	90                   	nop

res_test

0000000000003e60 <_ZN8asm_rust8res_test17hfbb14daa75e009ddE>:
    3e60:	50                   	push   %rax
    3e61:	c7 04 24 01 00 00 00 	movl   $0x1,(%rsp) ; 将0x1压入栈中
    3e68:	c7 44 24 04 02 00 00 	movl   $0x2,0x4(%rsp) ;0x2压入栈中
    3e6f:	00 
    3e70:	8b 04 24             	mov    (%rsp),%eax ; 将栈中的0x1送到eax
    3e73:	8b 54 24 04          	mov    0x4(%rsp),%edx ; 将栈中的0x2送到edx
    3e77:	59                   	pop    %rcx
    3e78:	c3                   	retq   ; eax和edx寄存器作为返回结果
    3e79:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

如果我们使用let (s1:i32,s2:i32) = res_test()方式调用后反汇编结果如下

0000000000003e30 <_ZN8asm_rust4main17hacd67c1ea264c8ceE>:
    3e30:	48 83 ec 18          	sub    $0x18,%rsp
    3e34:	e8 27 00 00 00       	callq  3e60 <_ZN8asm_rust8res_test17hfbb14daa75e009ddE>
    3e39:	89 44 24 0c          	mov    %eax,0xc(%rsp) ; 依旧使用的是eax,edx寄存器,元组中的值会存储在栈中,在使用时会从栈中取出
    3e3d:	89 54 24 08          	mov    %edx,0x8(%rsp) 
    3e41:	8b 44 24 0c          	mov    0xc(%rsp),%eax
    3e45:	89 44 24 10          	mov    %eax,0x10(%rsp)
    3e49:	8b 4c 24 08          	mov    0x8(%rsp),%ecx
    3e4d:	89 4c 24 14          	mov    %ecx,0x14(%rsp)
    3e51:	48 83 c4 18          	add    $0x18,%rsp
    3e55:	c3                   	retq   
    3e56:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
    3e5d:	00 00 00 

如果我们超过2个参数会发生什么

fn main() {
    let (s1,s2,s3) = res_test2();
}

pub fn res_test2() -> (i32,i32,i32){
    (1,2,3)
}

反汇编后结果如下

0000000000003e30 <_ZN8asm_rust4main17hacd67c1ea264c8ceE>:
    3e30:	48 83 ec 28          	sub    $0x28,%rsp ; 分配栈空间
    3e34:	48 8d 7c 24 18       	lea    0x18(%rsp),%rdi ; lea意思是load effect address(加载有效地址)使用rdi寄存器作为参数传递
    3e39:	e8 22 00 00 00       	callq  3e60 <_ZN8asm_rust9res_test217h5c3016b4f0e92cfbE>
    3e3e:	8b 44 24 18          	mov    0x18(%rsp),%eax ; 可以看到eax指向的是一个栈空间,使用时从栈中取出
    3e42:	89 44 24 0c          	mov    %eax,0xc(%rsp)
    3e46:	8b 44 24 1c          	mov    0x1c(%rsp),%eax
    3e4a:	89 44 24 10          	mov    %eax,0x10(%rsp)
    3e4e:	8b 44 24 20          	mov    0x20(%rsp),%eax
    3e52:	89 44 24 14          	mov    %eax,0x14(%rsp)
    3e56:	48 83 c4 28          	add    $0x28,%rsp
    3e5a:	c3                   	retq   
    3e5b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

res_test2函数返反汇编后结果如下

0000000000003e60 <_ZN8asm_rust9res_test217h5c3016b4f0e92cfbE>:
    3e60:	48 89 f8             	mov    %rdi,%rax ; rax保存了栈空间的起始地址
    3e63:	c7 07 01 00 00 00    	movl   $0x1,(%rdi) ; rdi作为函数参数传递将数值写入rdi所指向的空间中
    3e69:	c7 47 04 02 00 00 00 	movl   $0x2,0x4(%rdi)
    3e70:	c7 47 08 03 00 00 00 	movl   $0x3,0x8(%rdi)
    3e77:	c3                   	retq   ; rax作为返回结果
    3e78:	0f 1f 84 00 00 00 00 	nopl   0x0(%rax,%rax,1)
    3e7f:	00 

由此我们可以看出,当函数返回结果为1个时使用的是rax寄存器,当函数返回结果为2个时使用的是rax和rdx寄存器,当超过2个参数,rust会分配一个栈空间,栈空间的起始地址作为参数传递给rdi寄存器,并且rax寄存器作为返回值,不过rax保存的是栈空间的起始地址

向汇编函数传递指针

在rust中&T不仅仅是借用的含义,还有C语言中的取值功能,下面我们探索以下rust中的使用&T作为参数的函数调用过程

现在我们创建一个test函数,该函数功能含简单,将传递的值加一

fn main() {
    let a = 1;
    let  a = test(&a);
}

pub fn test(a:&i32) -> i32{
    a + 1
}

使用objdump反汇编后结果如下

0000000000003e40 <_ZN8asm_rust4main17hacd67c1ea264c8ceE>:
    3e40:	50                   	push   %rax
    3e41:	c7 44 24 04 01 00 00 	movl   $0x1,0x4(%rsp) ; 分配绑定a的空间 -> let a = 1;
    3e48:	00 
    3e49:	48 8d 7c 24 04       	lea    0x4(%rsp),%rdi ; 将a的地址传递给rdi寄存器,不同的是用的是lea指令而非mov指令
    3e4e:	e8 0d 00 00 00       	callq  3e60 <_ZN8asm_rust4test17h65f5bc503e4663fcE> ; 调用test函数 
    3e53:	58                   	pop    %rax
    3e54:	c3                   	retq   
    3e55:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
    3e5c:	00 00 00 
    3e5f:	90                   	nop

这段代码比较重要的是test函数调用后返回值没有任何绑定,如果使用如下方法来调用,对应的汇编代码会不同

fn main() {
    let a = 1;
    let a = test(&a);
}

pub fn test(a:&i32) -> i32{
    a + 1
}

反汇编后

0000000000003e40 <_ZN8asm_rust4main17hacd67c1ea264c8ceE>:
    3e40:	50                   	push   %rax
    3e41:	c7 04 24 01 00 00 00 	movl   $0x1,(%rsp) ; 分配绑定a的空间 let a = 1;
    3e48:	48 89 e7             	mov    %rsp,%rdi ; 可以看到这里用的是mov指令,直接传递栈指针
    3e4b:	e8 10 00 00 00       	callq  3e60 <_ZN8asm_rust4test17h65f5bc503e4663fcE>
    3e50:	89 44 24 04          	mov    %eax,0x4(%rsp) ; let a = test(&a);  虽然绑定依旧是s可以看到s所对应的地址发生了改变
    3e54:	58                   	pop    %rax
    3e55:	c3                   	retq   
    3e56:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
    3e5d:	00 00 00 

产生这个的原因应该是编译优化的结果,现在我们更改test函数

fn main() {
    let a = 1;
    let b:i32 = 5;
    let a = test(&a,&b);
}

pub fn test(a:&i32,b:&i32) -> i32{
    a + b
}

反汇编后结果如下

0000000000003e40 <_ZN8asm_rust4main17hacd67c1ea264c8ceE>:
    3e40:	48 83 ec 18          	sub    $0x18,%rsp
    3e44:	c7 44 24 0c 01 00 00 	movl   $0x1,0xc(%rsp)
    3e4b:	00 
    3e4c:	c7 44 24 10 05 00 00 	movl   $0x5,0x10(%rsp)
    3e53:	00 
    3e54:	48 8d 7c 24 0c       	lea    0xc(%rsp),%rdi ; 可以看到绑定a和绑定b均使用的是lea指令
    3e59:	48 8d 74 24 10       	lea    0x10(%rsp),%rsi
    3e5e:	e8 0d 00 00 00       	callq  3e70 <_ZN8asm_rust4test17h8560531f953f300cE>
    3e63:	89 44 24 14          	mov    %eax,0x14(%rsp) ; 返回值依旧使用的是eax寄存器
    3e67:	48 83 c4 18          	add    $0x18,%rsp
    3e6b:	c3                   	retq   
    3e6c:	0f 1f 40 00          	nopl   0x0(%rax)

test函数的反汇编结果如下

0000000000003e70 <_ZN8asm_rust4test17h8560531f953f300cE>:
    3e70:	48 83 ec 18          	sub    $0x18,%rsp ; 分配栈所使用的空间
    3e74:	48 89 7c 24 08       	mov    %rdi,0x8(%rsp)  ; 将a地址保存在栈中
    3e79:	48 89 74 24 10       	mov    %rsi,0x10(%rsp) ; 将b地址结果保存在栈中
    3e7e:	48 8b 7c 24 08       	mov    0x8(%rsp),%rdi  ; 因为要调用core::ops::arith::Add函数 因此需要重新加载rdi和rsi
    3e83:	48 8b 74 24 10       	mov    0x10(%rsp),%rsi
    3e88:	e8 53 01 00 00       	callq  3fe0 <_ZN64_$LT$$RF$i32$u20$as$u20$core..ops..arith..Add$LT$$RF$i32$GT$$GT$3add17h8b2e71d2eadf467aE> ; 与add函数的不同之处是,使用了core::ops::arith::Add进行了处理
    3e8d:	89 44 24 04          	mov    %eax,0x4(%rsp)
    3e91:	8b 44 24 04          	mov    0x4(%rsp),%eax
    3e95:	48 83 c4 18          	add    $0x18,%rsp ; 释放栈空间
    3e99:	c3                   	retq   ; eax寄存器保存返回值
    3e9a:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)

与add函数不同的是指针的传递会使用lea指令将结果放入rdi和rsi寄存器,然后通过core::ops::arith::Add函数来完成加法操作

core::ops::arith::Add反汇编结果如下

0000000000003fe0 <_ZN64_$LT$$RF$i32$u20$as$u20$core..ops..arith..Add$LT$$RF$i32$GT$$GT$3add17h8b2e71d2eadf467aE>:
    3fe0:	48 83 ec 18          	sub    $0x18,%rsp ; 分配栈所使用的空间
    3fe4:	48 89 7c 24 08       	mov    %rdi,0x8(%rsp) ; 将传入的2个参数压入栈中
    3fe9:	48 89 74 24 10       	mov    %rsi,0x10(%rsp)
    3fee:	48 8b 44 24 08       	mov    0x8(%rsp),%rax ; 将第一个参数送入eax中备用
    3ff3:	8b 38                	mov    (%rax),%edi ; 获取rax寄存器保存地址的值相当于C语言中的*rax(解引用)  获取第一个参数所指向地址的值
    3ff5:	48 8b 44 24 10       	mov    0x10(%rsp),%rax ; 将第一个参数送入eax中备用
    3ffa:	8b 30                	mov    (%rax),%esi ; 获取rax寄存器保存地址的值相当于C语言中的*rax(解引用)  获取第二个参数所指向地址的值
    3ffc:	e8 8f ff ff ff       	callq  3f90 <_ZN45_$LT$i32$u20$as$u20$core..ops..arith..Add$GT$3add17ha6f53134ef7c91eeE> ; 调用下层add方法
    4001:	89 44 24 04          	mov    %eax,0x4(%rsp)
    4005:	8b 44 24 04          	mov    0x4(%rsp),%eax
    4009:	48 83 c4 18          	add    $0x18,%rsp ; 释放栈空间
    400d:	c3                   	retq   ; eax寄存器保存返回值
    400e:	66 90                	xchg   %ax,%ax


0000000000003f90 <_ZN45_$LT$i32$u20$as$u20$core..ops..arith..Add$GT$3add17ha6f53134ef7c91eeE>:
    3f90:	48 83 ec 18          	sub    $0x18,%rsp ; 分配栈所使用的空间
    3f94:	89 7c 24 10          	mov    %edi,0x10(%rsp) ; 将传入的2个参数压入栈中
    3f98:	89 74 24 14          	mov    %esi,0x14(%rsp)
    3f9c:	8b 44 24 10          	mov    0x10(%rsp),%eax ; 将结果进行相加
    3fa0:	03 44 24 14          	add    0x14(%rsp),%eax
    3fa4:	0f 90 c1             	seto   %cl ; 检查加法操作是否溢出
    3fa7:	f6 c1 01             	test   $0x1,%cl ; 相当于 add $0x1,%cl 不同之处是test不会修改al的值,仅仅修改CF,OF,PF,ZF,SF等标志位
    3faa:	89 44 24 0c          	mov    %eax,0xc(%rsp)
    3fae:	75 09                	jne    3fb9 <_ZN45_$LT$i32$u20$as$u20$core..ops..arith..Add$GT$3add17ha6f53134ef7c91eeE+0x29> ; 如果溢出调用panic
    3fb0:	8b 44 24 0c          	mov    0xc(%rsp),%eax 
    3fb4:	48 83 c4 18          	add    $0x18,%rsp ; 释放栈空间
    3fb8:	c3                   	retq   ; eax寄存器保存返回值
    3fb9:	48 8d 3d 40 1a 02 00 	lea    0x21a40(%rip),%rdi        # 25a00 
    3fc0:	48 8d 15 e1 c6 22 00 	lea    0x22c6e1(%rip),%rdx        # 2306a8 <__init_array_end+0x38>
    3fc7:	48 8d 05 c2 c0 01 00 	lea    0x1c0c2(%rip),%rax        # 20090 <_ZN4core9panicking5panic17he6d6b86858b8480dE>
    3fce:	be 1c 00 00 00       	mov    $0x1c,%esi
    3fd3:	ff d0                	callq  *%rax
    3fd5:	0f 0b                	ud2    
    3fd7:	66 0f 1f 84 00 00 00 	nopw   0x0(%rax,%rax,1)
    3fde:	00 00 

可以看到如果使用传递的是指针(借用),rust会首先会调用i32指针所对应的core::ops::arith::Add函数,该函数会把将指针的值取出,然后调用i32的core::ops::arith::Add来完成加法操作(会产生2次调用)

上述过程为我们使用汇编函数来完成指针传递提供了指导思想,我们来试一试指针操作的add
mod.rs中添加test_p_add函数

//in src/asm/mod.rs
#[link(name = "test_asm", kind = "static")]
extern "C" {
    ...
    #[cfg_attr(link_name="test_p_add", target_env = "gnu")]
    pub fn test_p_add(a:&i32,b:&i32) -> i32;
}

然后在对应的asm.s文件中添加以下内容

.global test_p_add
.p2align 4
test_p_add:
    mov (%rdi),%eax
    add (%rsi),%eax
    retq

我们的main.rs中的内容如下

extern crate asm_rust;


fn main() {
    let a:i32 = 1;
    let b:i32 = 5;
    let s = unsafe{asm_rust::asm::test_p_add(&a,&b)};
    println!("The result of call test_p_add function is {} with &i32 as parameter",s);
}

使用cargo run后结果为

The result of call test_p_add function is 6 with &i32 as parameter

这样便完成了指针传递的操作

向汇编函数传递数组

fn main() {
    let arr = [1, 2, 4, 5];
    test_array(arr);
}

fn test_array(arr: [i32; 4]) {
    let mut a = 0;
    for i in 0..arr.len() {
        a += arr[i];
    }
}

下面是main函数反汇编的结果

0000000000004380 <_ZN8asm_rust4main17hacd67c1ea264c8ceE>:
    4380:	48 83 ec 28          	sub    $0x28,%rsp // 分配栈空间
    4384:	c7 44 24 08 01 00 00 	movl   $0x1,0x8(%rsp) // 将数组元素以此压入栈中 (第一个元素)
    438b:	00 
    438c:	c7 44 24 0c 02 00 00 	movl   $0x2,0xc(%rsp) // 第二个元素
    4393:	00 
    4394:	c7 44 24 10 04 00 00 	movl   $0x4,0x10(%rsp) // 第三个元素
    439b:	00 
    439c:	c7 44 24 14 05 00 00 	movl   $0x5,0x14(%rsp) // 第四个元素
    43a3:	00 
    43a4:	48 8b 44 24 08       	mov    0x8(%rsp),%rax // let arr = [1, 2, 4, 5];
    43a9:	48 89 44 24 18       	mov    %rax,0x18(%rsp)
    43ae:	48 8b 44 24 10       	mov    0x10(%rsp),%rax
    43b3:	48 89 44 24 20       	mov    %rax,0x20(%rsp)
    43b8:	48 8d 7c 24 18       	lea    0x18(%rsp),%rdi // 获取数组地址并将其作为参数
    43bd:	e8 0e 00 00 00       	callq  43d0 <_ZN8asm_rust10test_array17haf2b76693a522bf4E>
    43c2:	48 83 c4 28          	add    $0x28,%rsp
    43c6:	c3                   	retq   
    43c7:	66 0f 1f 84 00 00 00 	nopw   0x0(%rax,%rax,1)
    43ce:	00 00 

在调用test_array时,使用lea指令数组起始地址,因此rdi保存的是数组的起始地址,但是我们可以看到在调用时并没有传递数组的长度大小,因为rust数组在编译期已经确定大小我们可以从test_array函数的反汇编结果可以看到

以下是test_array反汇编的结果,有些长我们来一行一行解释

00000000000043d0 <_ZN8asm_rust10test_array17haf2b76693a522bf4E>:
    43d0:	48 81 ec 88 00 00 00 	sub    $0x88,%rsp // 分配栈所使用的空间
    43d7:	c7 44 24 3c 00 00 00 	movl   $0x0,0x3c(%rsp) //
    43de:	00 
    43df:	48 89 f8             	mov    %rdi,%rax
    43e2:	48 89 7c 24 30       	mov    %rdi,0x30(%rsp) // 参数1 移动到了0x30
    43e7:	48 89 c7             	mov    %rax,%rdi // 参数1:数组起始地址
    43ea:	be 04 00 00 00       	mov    $0x4,%esi // 参数2:数组长度
    43ef:	e8 5c fa ff ff       	callq  3e50 <_ZN4core5slice29_$LT$impl$u20$$u5b$T$u5d$$GT$3len17h0c2c7f5e426b4f2eE>  // 获取数组大小
    43f4:	48 89 44 24 28       	mov    %rax,0x28(%rsp) // 将计算出的数组长度压入栈中
    43f9:	48 c7 44 24 40 00 00 	movq   $0x0,0x40(%rsp) // 0x0压入栈中
    4400:	00 00 
    4402:	48 8b 44 24 28       	mov    0x28(%rsp),%rax // 数组的长度 -> rax
    4407:	48 89 44 24 48       	mov    %rax,0x48(%rsp) // rax值被移动到了 0x48位置
    440c:	48 8b 7c 24 40       	mov    0x40(%rsp),%rdi // 0x40和0x48的值分别为 0和 数组长度(4) 相当于 0..len(array) => 0..4
    4411:	48 8b 74 24 48       	mov    0x48(%rsp),%rsi
    4416:	e8 05 04 00 00       	callq  4820 <_ZN63_$LT$I$u20$as$u20$core.iter..traits..collect..IntoIterator$GT$9into_iter17hd6491db20f0d3ac5E> // 调用core::iter::traits::collect::IntoIterator(0,4)
    441b:	48 89 44 24 20       	mov    %rax,0x20(%rsp) // core::iter::traits::collect::IntoIterator(0,4)函数返回的元组分别草存在rax和rdx寄存器中
    4420:	48 89 54 24 18       	mov    %rdx,0x18(%rsp)
    4425:	48 8b 44 24 20       	mov    0x20(%rsp),%rax // 函数返回值1发生了移动
    442a:	48 89 44 24 50       	mov    %rax,0x50(%rsp)
    442f:	48 8b 4c 24 18       	mov    0x18(%rsp),%rcx // 函数返回值2发生了移动
    4434:	48 89 4c 24 58       	mov    %rcx,0x58(%rsp)
    4439:	48 8d 7c 24 50       	lea    0x50(%rsp),%rdi // 0x50保存的是返回值1的地址
    443e:	e8 cd 02 00 00       	callq  4710 <_ZN4core4iter5range101_$LT$impl$u20$core..iter..traits..iterator..Iterator$u20$for$u20$core..ops..range..Range$LT$A$GT$$GT$4next17h157a8bd2a96eab95E> // 该函数调用链过长不展开了
    4443:	48 89 54 24 70       	mov    %rdx,0x70(%rsp) // 返回值2
    4448:	48 89 44 24 68       	mov    %rax,0x68(%rsp) // 返回值1
    444d:	48 8b 44 24 68       	mov    0x68(%rsp),%rax
    4452:	48 85 c0             	test   %rax,%rax // 所有rax位都清0时,ZF置 1
    4455:	74 04                	je     445b <_ZN8asm_rust10test_array17haf2b76693a522bf4E+0x8b> // 判断ZF标志位 如果ZF置1 则跳转到 add    $0x88,%rsp处(445b: 处)
    4457:	eb 00                	jmp    4459 <_ZN8asm_rust10test_array17haf2b76693a522bf4E+0x89> // 如果ZF没有被置位则跳转到 4465:处 (最终会跳转到这里)
    4459:	eb 0a                	jmp    4465 <_ZN8asm_rust10test_array17haf2b76693a522bf4E+0x95>
    445b:	48 81 c4 88 00 00 00 	add    $0x88,%rsp // 释放分配的栈空间
    4462:	c3                   	retq   // 函数返回
    4463:	0f 0b                	ud2    
    4465:	48 8b 44 24 70       	mov    0x70(%rsp),%rax // 以下是令人窒息的操作。。。
    446a:	48 89 44 24 78       	mov    %rax,0x78(%rsp) 
    446f:	48 8b 44 24 78       	mov    0x78(%rsp),%rax 
    4474:	48 89 44 24 60       	mov    %rax,0x60(%rsp) 
    4479:	48 8b 44 24 60       	mov    0x60(%rsp),%rax 
    447e:	48 89 84 24 80 00 00 	mov    %rax,0x80(%rsp)  
    4485:	00 
    4486:	48 8b 84 24 80 00 00 	mov    0x80(%rsp),%rax // 窒息操作结束
    448d:	00 
    448e:	48 83 f8 04          	cmp    $0x4,%rax // 比较rax是否为4 如果rax大于4则置CF位
    4492:	0f 92 c1             	setb   %cl // 置CF标志位
    4495:	f6 c1 01             	test   $0x1,%cl // 检查索引是否越界
    4498:	48 89 44 24 10       	mov    %rax,0x10(%rsp)
    449d:	75 02                	jne    44a1 <_ZN8asm_rust10test_array17haf2b76693a522bf4E+0xd1> // 如果ZF没有被置位 跳转到44a1处
    449f:	eb 2c                	jmp    44cd <_ZN8asm_rust10test_array17haf2b76693a522bf4E+0xfd> // 如果ZF被置位跳转到44cd处(因为数组越界而panic) 
    44a1:	48 8b 44 24 30       	mov    0x30(%rsp),%rax // 数组array的起始地址
    44a6:	48 8b 4c 24 10       	mov    0x10(%rsp),%rcx // 获取i的值
    44ab:	8b 14 88             	mov    (%rax,%rcx,4),%edx // 将arr[i]移入edx
    44ae:	03 54 24 3c          	add    0x3c(%rsp),%edx // 与a的值相加
    44b2:	40 0f 90 c6          	seto   %sil // 检测加法是否溢出
    44b6:	40 f6 c6 01          	test   $0x1,%sil
    44ba:	89 54 24 0c          	mov    %edx,0xc(%rsp) // 保存相加的结果
    44be:	75 29                	jne    44e9 <_ZN8asm_rust10test_array17haf2b76693a522bf4E+0x119> // 如果溢出则调用panic
    44c0:	8b 44 24 0c          	mov    0xc(%rsp),%eax
    44c4:	89 44 24 3c          	mov    %eax,0x3c(%rsp) // 将结果压入0x3c位置
    44c8:	e9 6c ff ff ff       	jmpq   4439 <_ZN8asm_rust10test_array17haf2b76693a522bf4E+0x69> // 跳转到4439处(lea    0x50(%rsp),%rdi)
    44cd:	48 8d 3d d4 c1 22 00 	lea    0x22c1d4(%rip),%rdi        # 2306a8 <__init_array_end+0x38> 
    44d4:	48 8d 05 05 c5 01 00 	lea    0x1c505(%rip),%rax  //panic(数组越界)      # 209e0 <_ZN4core9panicking18panic_bounds_check17h189a5a5e8747cf2aE>
    44db:	ba 04 00 00 00       	mov    $0x4,%edx
    44e0:	48 8b 74 24 10       	mov    0x10(%rsp),%rsi
    44e5:	ff d0                	callq  *%rax
    44e7:	0f 0b                	ud2    
    44e9:	48 8d 3d d0 1d 02 00 	lea    0x21dd0(%rip),%rdi        # 262c0 
    44f0:	48 8d 15 c9 c1 22 00 	lea    0x22c1c9(%rip),%rdx        # 2306c0 <__init_array_end+0x50>
    44f7:	48 8d 05 92 c4 01 00 	lea    0x1c492(%rip),%rax    //panic(加法溢出)     # 20990 <_ZN4core9panicking5panic17he6d6b86858b8480dE>
    44fe:	be 1c 00 00 00       	mov    $0x1c,%esi
    4503:	ff d0                	callq  *%rax
    4505:	0f 0b                	ud2    
    4507:	66 0f 1f 84 00 00 00 	nopw   0x0(%rax,%rax,1)
    450e:	00 00 

以上是test_array反汇编的解读,因为rust在编译期已经知道数组大小,所以在rust调用时不需要传递数组大小,但是我们在编写汇编函数时需要传递数组的大小,并且我们不能像传递普通参数那样传递数组,必须将数组以裸指针的方式传递

知道了调用所需要的参数,我们可以着手编写汇编函数了

我们创建一个sum_array函数,该函数用来计算数组中所有元素之和,如果需要更改数组内的元素,则需要改为arr: *mut i32

// in src/asm/mod.rs
#[link(name = "test_asm", kind = "static")]
extern "C" {
    ...
    #[cfg_attr(link_name="sum_array", target_env = "gnu")]
    pub fn sum_array(arr: *const i32,size:usize)->i32;
}

对应的我们的汇编代码如下

// in src/asm/asm.s

.global sum_array
.p2align 4
sum_array:
    sub $0x4,%rsp   // 分配sum所需要的空间
    movl $0x0,(%rsp)// let sum = 0
    mov $0x0,%rcx // 循环次数 i
.1:
    movl (%rdi,%rcx,4),%edx // edx = arr[i]
    mov %edx,%eax 
    add (%rsp),%edx  // sum += edx
    mov %edx,(%rsp) 
    add $0x1,%rcx // i+=
    cmp %rsi,%rcx // if rcx != rsi
    jne .1 // goto .1

    mov (%rsp),%eax 
    add $0x4,%rsp
    retq

我们在main函数中调用刚才编写的汇编函数

extern crate asm_rust;

fn main() {
    let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    let res = unsafe { asm_rust::asm::sum_array(arr.as_ptr(), arr.len()) };
    println!("total={}", res);
}

使用cargo run运行后结果如下

total=55

向汇编函数传递结构体

使用T作为参数

在main.rs中添加一个Test结构,该结构很简单只有2个成员变量

struct Test {
    pub a: i32,
    pub b: i32,
}

fn main() {
    let a = Test{
        a:5,
        b:10,
    };
    test_struct(a);
}

fn test_struct(a:Test){
    let b = a.a;
}

通过objdump反汇编后结果如下:

0000000000003e30 <_ZN8asm_rust4main17hacd67c1ea264c8ceE>:
    3e30:	50                   	push   %rax
    3e31:	c7 04 24 05 00 00 00 	movl   $0x5,(%rsp)
    3e38:	c7 44 24 04 0a 00 00 	movl   $0xa,0x4(%rsp)
    3e3f:	00 
    3e40:	8b 3c 24             	mov    (%rsp),%edi
    3e43:	8b 74 24 04          	mov    0x4(%rsp),%esi
    3e47:	e8 04 00 00 00       	callq  3e50 <_ZN8asm_rust11test_struct17hf07262dcdcd6a217E>
    3e4c:	58                   	pop    %rax
    3e4d:	c3                   	retq   
    3e4e:	66 90                	xchg   %ax,%ax

我们可以看到Test结构体传递是最终当做2个单独的参数传递了,我们也可以从弄个test_struct函数反汇编结果来印证这一点

0000000000003e50 <_ZN8asm_rust11test_struct17hf07262dcdcd6a217E>:
    3e50:	48 83 ec 10          	sub    $0x10,%rsp
    3e54:	89 3c 24             	mov    %edi,(%rsp)
    3e57:	89 74 24 04          	mov    %esi,0x4(%rsp)
    3e5b:	8b 04 24             	mov    (%rsp),%eax
    3e5e:	89 44 24 0c          	mov    %eax,0xc(%rsp)
    3e62:	48 83 c4 10          	add    $0x10,%rsp
    3e66:	c3                   	retq   
    3e67:	66 0f 1f 84 00 00 00 	nopw   0x0(%rax,%rax,1)
    3e6e:	00 00 

使用&T作为参数

Test结构体保持不变,test_struct函数的参数改为&T的方式,

fn main() {
    let a = Test{
        a:5,
        b:10,
    };
    test_struct(&a);
}

fn test_struct(a:&Test){
    let b = a.a;
    let a = a.b;
}

反汇编后结果如下

0000000000003e30 <_ZN8asm_rust4main17hacd67c1ea264c8ceE>:
    3e30:	50                   	push   %rax
    3e31:	c7 04 24 05 00 00 00 	movl   $0x5,(%rsp)
    3e38:	c7 44 24 04 0a 00 00 	movl   $0xa,0x4(%rsp)
    3e3f:	00 
    3e40:	48 89 e7             	mov    %rsp,%rdi 
    3e43:	e8 08 00 00 00       	callq  3e50 <_ZN8asm_rust11test_struct17h2acf1e8eb4c7fdb9E>
    3e48:	58                   	pop    %rax
    3e49:	c3                   	retq   
    3e4a:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)

我们可以看到传递参数的方式发生了改变,相应的test_struct函数的内容也发生了改变

0000000000003e50 <_ZN8asm_rust11test_struct17h2acf1e8eb4c7fdb9E>:
    3e50:	48 83 ec 10          	sub    $0x10,%rsp ; 分配栈使用的空间
    3e54:	48 89 3c 24          	mov    %rdi,(%rsp) ; 将传递的参数压入栈中
    3e58:	48 8b 04 24          	mov    (%rsp),%rax ; 将&T解引用获取具体数值
    3e5c:	8b 08                	mov    (%rax),%ecx ; let b = a.a; 
    3e5e:	89 4c 24 08          	mov    %ecx,0x8(%rsp) ; 将b的值压回栈中 drop(b)
    3e62:	48 8b 04 24          	mov    (%rsp),%rax ; 将&T解引用获取具体数值
    3e66:	8b 48 04             	mov    0x4(%rax),%ecx ; let a = a.b;
    3e69:	89 4c 24 0c          	mov    %ecx,0xc(%rsp) ; 将a的值压回栈中 drop(a)
    3e6d:	48 83 c4 10          	add    $0x10,%rsp ; 回收分配的栈空间
    3e71:	c3                   	retq   ; 函数返回
    3e72:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
    3e79:	00 00 00 
    3e7c:	0f 1f 40 00          	nopl   0x0(%rax)

我们可以看到调用Test中的b成员需要首先将&Test解引用获取具体数据,然后通过偏移来获取,i32占用的4字节,因此0x4(%rax)便获取了b成员的值

但是对于汇编调用而言我们并不能像Rust那样使用这应不是FFI安全的,可能会造成未定义行为,因此我们需要对结构体做一些特殊操作

例如我们如果创建这样的一个struct

pub struct TestStruct {
    pub a: i32,
    pub b: i32,
}
// 对应的函数为
#[cfg_attr(link_name="test_struct", target_env = "gnu")]
pub fn test_struct(arr: &TestStruct)->i32;

对应的汇编函数如下

.global test_struct
.p2align 4
test_struct:
    leaq (%rdi),%rsi

    movl (%rsi),%eax
    movl 4(%rsi),%ecx
    add %ecx,%eax

    retq

对应的main函数

fn main() {
    let a = &TestStruct{
        a:1,
        b:1,
    };
    let res = unsafe{asm_rust::asm::test_struct(a)};
    println!("res : {}",res)
}

该函数返回将a和b成员相加结果,因为我们传递的是指针因此需要使用lea指令加载指针对应的值

当我们使用cargo run 的时候将会出现警告

warning: `extern` block uses type `asm::TestStruct`, which is not FFI-safe
  --> src/asm/mod.rs:18:29
   |
18 |     pub fn test_struct(arr: &TestStruct)->i32;
   |                             ^^^^^^^^^^^ not FFI-safe
   |
   = note: `#[warn(improper_ctypes)]` on by default
   = help: consider adding a `#[repr(C)]` or `#[repr(transparent)]` attribute to this struct
   = note: this struct has unspecified layout

可以看到TestStruct并不是FFI-safe并且在下方提示了添加#[repr(C)]
当我们为TestStruct结构体添加该属性后就会编译成功并运行成功

注意事项
在编写过程中需要注意以下事项

  1. 传递参数时需要按照C ABI调用约定和内存布局传递
  2. 知悉传递的每个参数的大小,例如在x86-64架构中指针占用8字节,i32占用4字节
  3. 编写尽量少的汇编代码

这样我们就完成了不使用nightly的编译器的方式完成了嵌入汇编操作

你可能感兴趣的:(Rust点点滴滴)