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
来指定特定的寄存器。