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 <venmosnake@yeah.net>"]
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::<Vec<_>>();    // 编译寻找到的.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    %rax3e45:    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 如果设置了溢出标志,则将操作数中的字节设置为13e67:    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的panic3e70:    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 <str.0>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,%esi3e93:  ff d0                   callq  *%rax3e95:   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 // 第一个参数0x13e39:   be 02 00 00 00          mov    $0x2,%esi // 第二个参数0x23e3e:   ba 03 00 00 00          mov    $0x3,%edx // 第三个参数0x33e43:   b9 04 00 00 00          mov    $0x4,%ecx // 第四个参数0x43e48:   41 b8 05 00 00 00       mov    $0x5,%r8d // 第五个参数0x53e4e:   41 b9 06 00 00 00       mov    $0x6,%r9d // 第六个参数0x63e54:   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,%rsp3e78:  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 // 如果设置了溢出标志,则将操作数中的字节设置为13eb8:   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的panic3ec1:    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 <str.0>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,%esi3ee5:  ff d0                   callq  *%rax3ee7:   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,%rsp3e34:  bf 01 00 00 00          mov    $0x1,%edi3e39:   be 02 00 00 00          mov    $0x2,%esi3e3e:   ba 03 00 00 00          mov    $0x3,%edx3e43:   b9 04 00 00 00          mov    $0x4,%ecx3e48:   41 b8 05 00 00 00       mov    $0x5,%r8d3e4e:   41 b9 06 00 00 00       mov    $0x6,%r9d3e54:   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,%rsp3e78:  c3                      retq   3e79:    0f 1f 80 00 00 00 00    nopl   0x0(%rax)// fn add
0000000000003e80 <_ZN8asm_rust3add17h10903d3f30e2f65dE>:3e80: 53                      push   %rbx3e81:    48 83 ec 20             sub    $0x20,%rsp3e85:  8b 44 24 40             mov    0x40(%rsp),%eax3e89: 44 8b 54 24 38          mov    0x38(%rsp),%r10d3e8e:    44 8b 5c 24 30          mov    0x30(%rsp),%r11d3e93:    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),%ecx3eb1:  03 4c 24 0c             add    0xc(%rsp),%ecx3eb5:  0f 90 c3                seto   %bl3eb8: f6 c3 01                test   $0x1,%bl3ebb:    89 4c 24 04             mov    %ecx,0x4(%rsp)3ebf:  75 0a                   jne    3ecb <_ZN8asm_rust3add17h10903d3f30e2f65dE+0x4b>3ec1: 8b 44 24 04             mov    0x4(%rsp),%eax3ec5:  48 83 c4 20             add    $0x20,%rsp3ec9:  5b                      pop    %rbx3eca:    c3                      retq   3ecb:    48 8d 3d ce 1a 02 00    lea    0x21ace(%rip),%rdi        # 259a0 <str.0>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,%esi3ee5:  ff d0                   callq  *%rax3ee7:   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,%eaxadd %esi,%eaxretq

然后我们在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   %rax3e31:    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    %rax3e3e:    c3                      retq   3e3f:    90                      nop

res_test

0000000000003e60 <_ZN8asm_rust8res_test17hfbb14daa75e009ddE>:3e60: 50                      push   %rax3e61:    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送到eax3e73:  8b 54 24 04             mov    0x4(%rsp),%edx ; 将栈中的0x2送到edx3e77:   59                      pop    %rcx3e78:    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,%rsp3e34:  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),%eax3e45:  89 44 24 10             mov    %eax,0x10(%rsp)3e49: 8b 4c 24 08             mov    0x8(%rsp),%ecx3e4d:  89 4c 24 14             mov    %ecx,0x14(%rsp)3e51: 48 83 c4 18             add    $0x18,%rsp3e55:  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),%eax3e4a: 89 44 24 10             mov    %eax,0x10(%rsp)3e4e: 8b 44 24 20             mov    0x20(%rsp),%eax3e52: 89 44 24 14             mov    %eax,0x14(%rsp)3e56: 48 83 c4 28             add    $0x28,%rsp3e5a:  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   %rax3e41:    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    %rax3e54:    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   %rax3e41:    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    %rax3e55:    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,%rsp3e44:  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),%rsi3e5e: 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,%rsp3e6b:  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和rsi3e83:  48 8b 74 24 10          mov    0x10(%rsp),%rsi3e88: 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),%eax3e95:  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),%eax4009:  48 83 c4 18             add    $0x18,%rsp ; 释放栈空间400d:  c3                      retq   ; eax寄存器保存返回值400e:   66 90                   xchg   %ax,%ax0000000000003f90 <_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),%eax3fa4: 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> ; 如果溢出调用panic3fb0: 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 <str.0>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,%esi3fd3:  ff d0                   callq  *%rax3fd5:   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),%eaxadd (%rsi),%eaxretq

我们的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),%rax43b3: 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,%rsp43c6:  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,%rax43e2:   48 89 7c 24 30          mov    %rdi,0x30(%rsp) // 参数1 移动到了0x3043e7: 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 // 数组的长度 -> rax4407:  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..44411:  48 8b 74 24 48          mov    0x48(%rsp),%rsi4416: 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) // 返回值24448: 48 89 44 24 68          mov    %rax,0x68(%rsp) // 返回值1444d: 48 8b 44 24 68          mov    0x68(%rsp),%rax4452: 48 85 c0                test   %rax,%rax // 所有rax位都清0时,ZF置 14455:    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]移入edx44ae:  03 54 24 3c             add    0x3c(%rsp),%edx // 与a的值相加44b2:   40 0f 90 c6             seto   %sil // 检测加法是否溢出44b6:    40 f6 c6 01             test   $0x1,%sil44ba:   89 54 24 0c             mov    %edx,0xc(%rsp) // 保存相加的结果44be:   75 29                   jne    44e9 <_ZN8asm_rust10test_array17haf2b76693a522bf4E+0x119> // 如果溢出则调用panic44c0:    8b 44 24 0c             mov    0xc(%rsp),%eax44c4:  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,%edx44e0:   48 8b 74 24 10          mov    0x10(%rsp),%rsi44e5: ff d0                   callq  *%rax44e7:   0f 0b                   ud2    44e9:    48 8d 3d d0 1d 02 00    lea    0x21dd0(%rip),%rdi        # 262c0 <str.0>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,%esi4503:  ff d0                   callq  *%rax4505:   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 = 0mov $0x0,%rcx // 循环次数 i
.1:movl (%rdi,%rcx,4),%edx // edx = arr[i]mov %edx,%eax add (%rsp),%edx  // sum += edxmov %edx,(%rsp) add $0x1,%rcx // i+=cmp %rsi,%rcx // if rcx != rsijne .1 // goto .1mov (%rsp),%eax add $0x4,%rspretq

我们在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   %rax3e31:    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),%edi3e43: 8b 74 24 04             mov    0x4(%rsp),%esi3e47:  e8 04 00 00 00          callq  3e50 <_ZN8asm_rust11test_struct17hf07262dcdcd6a217E>3e4c:  58                      pop    %rax3e4d:    c3                      retq   3e4e:    66 90                   xchg   %ax,%ax

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

0000000000003e50 <_ZN8asm_rust11test_struct17hf07262dcdcd6a217E>:3e50: 48 83 ec 10             sub    $0x10,%rsp3e54:  89 3c 24                mov    %edi,(%rsp)3e57: 89 74 24 04             mov    %esi,0x4(%rsp)3e5b:  8b 04 24                mov    (%rsp),%eax3e5e: 89 44 24 0c             mov    %eax,0xc(%rsp)3e62:  48 83 c4 10             add    $0x10,%rsp3e66:  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   %rax3e31:    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    %rax3e49:    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),%rsimovl (%rsi),%eaxmovl 4(%rsi),%ecxadd %ecx,%eaxretq

对应的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的各种花式汇编操作相关推荐

  1. 汇编语言基础--汇编操作指令概述

    本文是接续"汇编语言基础--机器级数据存储",主要介绍汇编指令的构造.寻址和指令主要分类. 操作指令 指令的基本要素:       在"计算机处理器(CPU)基础&quo ...

  2. python怎么把excel单元格里面的文字提取出来_干货 | Excel如何花式秀操作?

    求职工作,Excel必不可少 百度曾开价两万寻找精通Excel的数据人才 Excel不熟练还可能被辞退... 都知道Excel重要,如何快速提高? Excel不仅是制表工具 你可能觉得Excel只是个 ...

  3. 漫步者lollipods如何调节音量_看双麦降噪如何花式秀操作,漫步者Lollipods测评

    真无线蓝牙耳机从什么时候开始盛行? 自从某大厂带头取消掉3.5mm耳机孔之后,往后各厂家也陆续纷纷效仿,导致有线耳机用着也不怎么痛快了.要么给有线耳机再加一小段转接头.要么买干脆换一个跟手机充电口一样 ...

  4. 【Rust 日报】2021-09-16 Pipette: 一个模仿 Elixir 的管道操作的包,没有使用宏

    Pipette: 一个模仿 Elixir 的管道操作的包,没有使用宏 Elixir 是一门函数式编程语言,其中有个管道操作符十分好用,可以将上一步操作的结果传入给下一个方法,做链式调用,在某些情况下比 ...

  5. 用 Rust 开发 Linux,可行吗?

    作者 | 马超 出品 | CSDN(ID:CSDNnews) 继Python之后,Rust最近也火爆得出了圈,目前Rust在Serverless等很多云原生领域已经稳定占据了C位,那么让Rust更进一 ...

  6. 编写完10万行代码,我发了篇长文吐槽Rust

    机器之心编译 机器之心编辑部 存在一种完美的编程语言吗? Rust 语言因其并发安全性而深受众多开发者的喜爱,曾在多个榜单上获评最受欢迎编程语言.然而,现在有人花费大量时间编写 10 万行 Rust ...

  7. 【连载】 两百行Rust代码解析绿色线程原理(一)绪论及基本概念

    原文: Green threads explained in 200 lines of rust language 地址: https://cfsamson.gitbook.io/green-thre ...

  8. GCC 编译 C++ 程序分步骤流程(预处理 gcc -E、编译 gcc -S、汇编 gcc -c 和链接 gcc 以及 gcc -o 选项)

    C 或者 C++ 程序从源代码生成可执行程序的过程,需经历 4 个过程,分别是预处理.编译.汇编和链接. 同样,使用 GCC 编译器编译 C 或者 C++ 程序,也必须要经历这 4 个过程.但考虑在实 ...

  9. gcc——预处理(预编译),编译,汇编,链接

    一,预编译 操作步骤:gcc -E hello.c -o hello.i 主要作用: 处理关于 "#" 的指令 [1]删除#define,展开所有宏定义.例#define port ...

  10. 【C 语言】编译过程 分析 ( 预处理 | 编译 | 汇编 | 链接 | 宏定义 | 条件编译 | 编译器指示字 )

    相关文章链接 : 1.[嵌入式开发]C语言 指针数组 多维数组 2.[嵌入式开发]C语言 命令行参数 函数指针 gdb调试 3.[嵌入式开发]C语言 结构体相关 的 函数 指针 数组 4.[嵌入式开发 ...

最新文章

  1. 【深度学习】基于Pytorch的卷积神经网络概念解析和API妙用(二)
  2. 网站SEO优化之如何提升访客量?
  3. python散点图点的大小-Python散点图。 标记的大小和样式
  4. 浅谈百度新一代query-ad 推荐引擎如何提升广告收益率
  5. MVC 服务器文件下载
  6. egg框架访问 Mysql 数据库 egg-mysql 增删改查
  7. opencv复杂变换cvPyrDown [6]
  8. delphi 获取打印机默认纸张_如何设置一台打印机打印不同尺寸的纸张
  9. Android xml文件的序列化
  10. App Store新规即将到来 你准备好了吗?
  11. XManager连接CentOS6.5
  12. sql字段合并mysql_sql合并字段
  13. 好东西都在这里,不点下看看吗(博客目录导航,持续更新中...)
  14. html页面调节图片大小,怎么用css设置图片大小?
  15. matlab彩色转灰度图代码,彩色图转灰度图 matlab 实现代码
  16. 单声道蓝牙实现音乐播放
  17. 灰关联分析与语音/音乐信号识别
  18. 中科创达怎么样-融合智能工业视觉平台再获奖项
  19. 首域金融BOSCTIME_关于首域金融BOSCTIME|首域金融资料
  20. 虾图排名第四大的科技公司,你猜是哪家?

热门文章

  1. 坐标转换-大地转高斯平面平面坐标转换
  2. The field imgFile exceeds its maximum permitted size of 1048576 bytes.
  3. 详细分析”百词斩“数据库,如何实现一个良好的数据库系统?
  4. 机器学习——数学基础1,方差平方差标准差均方误差均方根误差
  5. 七牛云存储之文件上传(Android)
  6. Word文档最后一页总是删除不掉怎么办?
  7. ecshop多国货币汇率换算,多国货币切换,多国货币价格转换
  8. 手游内存辅助开发教程
  9. SAP 费用分摊分配用法(KSU5/KSV5/KB21N/KB11N)
  10. 【高等数学】平面束方程的俩种设法与其中一种设法会出现的漏解问题