rust学习之unsafe语块

unsafe为我们提供了解引用裸指针、调用一个 unsafe 或外部的函数、访问或修改一个可变的静态变量、实现一个 unsafe 特征、访问 union 中的字段这5个方面的能力,此时编译器不会进行内存安全方面的检查,使我们能轻松绕过相关的编译检查。但unsafe 并不能绕过 Rust 的借用检查,也不能关闭任何 Rust 的安全检查规则。

解引用裸指针

裸指针(raw pointer,又称原生指针) 长这样: *const T 和 *mut T,它们分别代表了不可变和可变。裸指针与 引用、智能指针不同,具有以下独特性:

  • 可以绕过 Rust 的借用规则,可以同时拥有一个数据的可变、不可变指针,甚至还能拥有多个可变的指针
  • 并不能保证指向合法的内存
  • 可以是 null
  • 没有实现任何自动的回收 (drop)
let mut num = 5;

let r1 = &num as *const i32; 
let r2 = &mut num as *mut i32;
let c: *const i32 = &mut; //隐式的转换方式
unsafe {
    println!("{}", *c);
}

 以上代码基于值的引用同时创建了可变和不可变的裸指针。as 可以用于强制类型转换,我们将引用 &num / &mut num 强转为相应的裸指针 *const i32 / *mut i32。使用 * 可以对裸指针 c 进行解引用。创建裸指针是安全的行为,而解引用裸指针才是不安全的行为,由于该指针的内存安全性并没有任何保证,因此我们需要使用 unsafe 来包裹解引用的逻辑。

虽然也可以基于一个现有的内存地址来创建裸指针,这种行为是相当危险的,因为试图使用任意的内存地址往往是一种未定义的行为。

还有一种是基于智能指针来创建裸指针的方式:

let a: Box<i32> = Box::new(10);
// 使用new需要先解引用a
let b: *const i32 = &*a; //隐式转换
// 使用 into_raw 来直接创建裸指针
let c: *const i32 = Box::into_raw(a);

 调用unsafe函数

使用 unsafe fn 来定义unsafe函数,并在调用该函数时加上unsafe 语句块,以此来强调正在调用一个不安全的函数。在unsafe函数体中无需再使用 unsafe 语句块。同样的,一个函数如果包含了unsafe代码,不代表我们需要将整个函数都定义为 unsafe fn

use std::slice;

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr();

    assert!(mid <= len);

    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    let mut v = vec![1, 2, 3, 4, 5, 6];

    let r = &mut v[..];

    let (a, b) = split_at_mut(r, 3);

    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5, 6]);
}
  • as_mut_ptr 会返回指向 slice 首地址的裸指针 *mut i32
  • slice::from_raw_parts_mut 函数通过指针和长度来创建一个新的切片
  • ptr.add(mid) 可以获取第二个切片的初始地址

由于 slice::from_raw_parts_mut 使用裸指针作为参数,因此它是一个 unsafe fn,我们在使用它时,就必须用 unsafe 语句块进行包裹。虽然 split_at_mut 使用 unsafe,但我们无需将其声明为 unsafe fn 

FFI——语言交互接口

通过 FFI(Foreign Function Interface), Rust 代码可以跟其它语言的外部代码进行交互。例如,在rust中调用 C 标准库中的 abs 函数: 

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}

在 extern "C" 代码块中,列出想要调用的外部函数的签名。其中 "C" 定义了外部函数所使用的应用二进制接口ABI (Application Binary Interface):ABI 定义了如何在汇编层面来调用该函数。而 extern 必须使用 unsafe 才能进行进行调用,因为Rust 无法对这些代码进行检查,需靠开发人员自行保证程序的安全性。同样,其他语言也可以利用 Rust 的生态,只需要使用 extern 来创建一个接口,其它语言可以通过该接口来调用相关的 Rust 函数。

 #[no_mangle]

pub extern "C" fn call_from_c() {

       println!("Just called a Rust function from C!");

}

 将其编译成一个共享库,然后链接到 C 语言中, call_from_c 函数就可以被C语言的代码所调用,注解 #[no_mangle]主要用于告诉 Rust 编译器不要修改函数的名称。

社区也还有一些使用的工具,例如,rust-bindgen 用于在 Rust 中访问 C 代码,而 cbindgen则反之。这两个库可以帮我们自动生成相应的接口。如果需要跟 C++ 代码交互,非常推荐使用 cxx

实现unsafe特征与访问union中的字段

unsafe 特征的声明步骤如下:

unsafe trait Foo {
    // 方法列表
}
unsafe impl Foo for i32 {
    // 实现相应的方法
}
fn main() {}

通过 unsafe impl 告诉编译器相应的正确性由我们自己来保证。例如之前说过的Send 特征标记为unsafe, 是因为 Rust 无法验证我们的类型是否能在线程间安全的传递。

union主要用于跟 C 代码进行交互。由于Rust 无法保证当前存储在 union 实例中的数据类型,因此访问 union 的字段是不安全的,union中所有字段都共享同一个存储空间,意味着往 union 的某个字段写入值,会导致其它字段的值会被覆盖。

#[repr(C)]
union MyUnion {
    f1: u32,
    f2: f32,
}

对于编译器来说,Rust 代码首先会被编译为 MIR ,然后再提交给 LLVM 进行处理,而miri 可以生成 Rust 的中间层表示 MIR,帮助我们检查常见的未定义行为,识别被执行代码路径的风险,但那些未被执行到的代码是没办法被识别。

可以通过 rustup component add miri 安装,并通过 cargo miri 来使用,同时还可以使用 cargo miri test 来运行测试代码。

miri可检查的部分未定义行为:

  • 内存越界检查和内存释放后再使用(use-after-free)
  • 使用未初始化的数据
  • 数据竞争
  • 内存对齐问题

内联汇编 

Rust 提供了 asm! 宏,可以在 Rust 代码中嵌入汇编代码

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,
    );
  //asm!("add {0}, 5", inout(reg) i); 输入输出使用同一个寄存器
  //asm!("add {0}, 5", inout(reg) i => o); 指定不同的输入和输出
}
assert_eq!(o, 8);

由于是插入的汇编语句,unsafe 语句块依然是必不可少的。上面代码将插入mov、add指令到编译器生成的汇编代码中,其中指令作为 asm! 的参数传入。

  • asm! 允许使用多个格式化字符串,每一个作为单独一行汇编代码存在,
  • in/out指明目标变量是作为内联汇编的输入还是输出
  • 和格式化字符串一样,可以使用多个参数,通过 {0}, {1} 来指定
  • 指定变量将要使用的寄存器,如通用寄存器reg,也可显示指定寄存器

以上代码表示的就是:将i的值拷贝到输出0,然后再加上 5。 inout 关键字说明 i 既是输入又是输出,可以保证使用同一个寄存器来完成任务。

Rust 提供一个 lateout 关键字,可以用于任何只在所有输入被消费后才被写入的输出,类似的还有inlateout

显式寄存器操作数无法用于格式化字符串中,例如我们之前使用的 {},只能直接在字符串中使用 eax。同时,该操作数只能出现在最后,也就是在其它操作数后面出现

use std::arch::asm;

let cmd = 0xd1;
unsafe {
    asm!("out 0x64, eax", in("eax") cmd);
}

上面的例子调用 out 指令将 cmd 变量的值输出到 0x64 内存地址中。由于 out 指令只接收 eax 和它的子寄存器,因此我们需要使用 eax 来指定特定的寄存器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值