unsafe和无定义行为
普通的代码就像rust和你签的契约,你遵守契约,rust保证你的安全,包括内存安全和没有无定义的行为。unsafe代码就是你告诉rust我在这里不像遵守契约,同时我也会承担其带来的责任。
unsafe代码块
unsafe代码块可以解锁五个功能:
- 可以调用unsafe函数。
- 可以使用raw pointer,普通代码只能创建和传递raw pointer。
- 可以访问union的字段。
- 可以使用可变的static 变量。
- 可以使用ffi。
是有unsafe代码块提议提醒写的人真的需要这样吗?也提醒review代码的人这里要注意。
unsafe代码块所造成的影响不一定只发生在代码块里,有可能是因为从代码块之前获取的变量所造成,返回的值也可能对后面的代码带来影响。
选择用unsafe代码块还是unsafe函数:如果使用该函数可以通过编译但还是有可能造成无定义后果的话,必须把函数定义成unsafe。
无定义行为
无定义行为(undefined behavior):Rust很确定的认为你的程序不会展现的行为。
编译器的工作是把一种语言编译成另外一种语言,如果翻译前后两种语言的行为一样。就认为两个程序是一样的。
fn very_trustworthy(shared: &i32) {
unsafe {
// Turn the shared reference into a mutable pointer.
// This is undefined behavior.
let mutable = shared as *const i32 as *mut i32;
*mutable = 20;
}
}
fn main() {
let i = 10;
very_trustworthy(&i);
println!("{}", i * 100);
}
如果只看main,因为i是不可变的,我们期望打印的结果是1000,但因为very_trustworthy把不可变引用变成了可变的,它就打破了我们的期望。
Rust对于有定义的行为(well-behaved)的程序的规则:
- 程序不能读取未初始化的内存
- 程序不能创建无效的基础类型:
- 空引用,或者空的Box,fn指针
- 既不是0也不是1的bool
- 包含无效判别式的enum
- 无效Unicode码的char
- 包含无效utf-8的str
- 无效长度的胖指针
- 任何类型是
!
的值
- 引用的规则:
- 引用的生存周期不能超过其引用的值
- 共享引用不能修改
- 可变引用不可共享
- 程序不能取空指针的值
- 程序不能去读超过指针所关联的范围的地址
- 不能有数据竞争
- 不能unwinding跨越ffi
- 程序必须遵守标准库的规则
所有违反上面这些规定的都是无定义行为。所有没用到unsafe的代码,rust都保证是有定义的。
unsafe trait
定义pub unsafe trait Zeroable {}
实现unsafe impl Zeroable for u8 {}
Zeroable trait表示那些可以安全的初始化成0的类型,比如数字,但有些是不能的比如引用。这些类型可以用std::ptr::write_bytes快速的初始化成0。
use core::nonzero::Zeroable;
fn zeroed_vector<T>(len: usize) -> Vec<T>
where T: Zeroable
{
let mut vec = Vec::with_capacity(len);
unsafe {
std::ptr::write_bytes(vec.as_mut_ptr(), 0, len);
vec.set_len(len);
}
vec
}
et v: Vec<usize> = zeroed_vector(100_000);
assert!(v.iter().all(|&u| u == 0));
不只unsafe代码要小心,safe的trait也有可能没有正确的实现,比如Hash如果实现的函数每次返回的哈希值都不一样。
raw pointer
raw pointer可以用来构建一些Rust的安全的指针构建不了的结构,比如双向链表,任意的图结构。因为raw pointer这么灵活,所以Rust无法判断其是否安全,所以只能用在unsafe代码里。只有使用(取值和写值)是需要unsafe代码块的,创建和传递,比较不用。
因为raw pointer和C/C++的指针很像,所以也用来和这些语言交互。
两种raw pointer:
- *mut T,可以修改所指的内容
- *const T,只能读
创建raw pointer
可以通过将引用做类型转换的方式创建raw pointer。通过*
取值。
let mut x = 10;
let ptr_x = &mut x as *mut i32;
let y = Box::new(20);
let ptr_y = &*y as *const i32;
unsafe {
*ptr_x += *ptr_y;
}
assert_eq!(x, 30);
和Rust的其他指针不同,raw pointer可以为null。
fn option_to_raw<T>(opt: Option<&T>) -> *const T {
match opt {
None => std::ptr::null(),
Some(r) => r as *const T
}
}
assert!(!option_to_raw(Some(&("pea", "pod"))).is_null());
assert_eq!(option_to_raw::<i32>(None), std::ptr::null());
指向不确定大小的值的raw pointer是fat pointer,
const [u8]包含地址和长度。
trait object如`mut dyn std::io::Write`包含vtable。
安全的指针类型在很多情况下都会隐式取值,raw pointer必须显式的取值。
C/C++可以用类似+的操作符对指针进行计算,但raw pointer不行,只能通过其方法offset和wrapping_offset等。
let trucks = vec!["garbage truck", "dump truck", "moonstruck"];
let first: *const &str = &trucks[0];
let last: *const &str = &trucks[2];
assert_eq!(unsafe { last.offset_from(first) }, 2);
assert_eq!(unsafe { first.offset_from(last) }, -2);
生成raw pointer时只要写明类型,rust会自动从普通引用转换,但反过来不行。也就是说从安全到不安全很容易,从不安全去安全有限制。
在转换的时候可能需要分步
&vec![42_u8] as *const String; // error: invalid conversion
&vec![42_u8] as *const Vec<u8> as *const String; // permitted
从raw转安全必须在一个unsafe代码里,先取值,然后再从值里借一个引用。但是要注意其生命周期是不确定的。
有些类型提供方法生成raw pointer:as_ptr,as_mut_ptr。有些指针类有转换的放Box, Rc, 和 Arc:into_raw and from_raw 。
也可以从整形生成。
RefWithFlag
一个例子,对于内存对齐在偶数地址的类型,由于其地址的最后一位永远是0,所以可以用来存储一个bit额外的信息,这里用来存一个bool。
mod ref_with_flag {
use std::marker::PhantomData;
use std::mem::align_of;
/// A `&T` and a `bool`, wrapped up in a single word.
/// The type `T` must require at least two-byte alignment.
///
/// If you're the kind of programmer who's never met a pointer whose
/// 2⁰-bit you didn't want to steal, well, now you can do it safely!
/// ("But it's not nearly as exciting this way...")
pub struct RefWithFlag<'a, T> {
ptr_and_bit: usize,
behaves_like: PhantomData<&'a T> // occupies no space
}
impl<'a, T: 'a> RefWithFlag<'a, T> {
pub fn new(ptr: &'a T, flag: bool) -> RefWithFlag<T> {
assert!(align_of::<T>() % 2 == 0);
RefWithFlag {
ptr_and_bit: ptr as *const T as usize | flag as usize,
behaves_like: PhantomData
}
}
pub fn get_ref(&self) -> &'a T {
unsafe {
let ptr = (self.ptr_and_bit & !1) as *const T;
&*ptr
}
}
pub fn get_flag(&self) -> bool {
self.ptr_and_bit & 1 != 0
}
}
}
因为从raw取值再取引用获取的引用的生命周期是不确定的,所以这里在创建的RefWithFlag的时候T就有一个声明周期参数’a,所以这里返回的&*ptr也是&’a T就有生命周期。因为存储指针和flag的字段ptr_and_bit是个usize,本身是不能保存生命周期的,所以behaves_like: PhantomData<&’a T>的作用就是用来存生命周期的,PhantomData本身又不占内存。
null
地址值为0的raw pointer就是一个null pointer。std::ptr::null
可以用is_null方法判断是否为空指针。as_ref 和as_mut 更方便。
内存大小和对齐
std::mem::size_of::
对齐必须是2的幂(1算是2的0次幂?bool和u8,i8的对齐是1)。类型的size会取整成对齐的倍数。(f32, u8)虽然可以只占5个字节,但其对齐是4,所以其size就是8。
对于unsized类型,其大小需要看值。用方法std::mem::size_of_val 和 std::mem::align_of_val获取。但这两个方法可以用来操作unsized和sized。
指针的数值计算
Rust的数组的内存layout就是按元素的size一个接着一个排的,如果第i个元素的位置就是从第一个元素开始数第isize个字节的位置。
这样用raw pointer取元素的值就可以用ptr+(isize)获得。offset方法基本就是这样的:
fn offset<T>(ptr: *const T, count: isize) -> *const T
where T: Sized
{
let bytes_per_element = std::mem::size_of::<T>() as isize;
let byte_offset = count * bytes_per_element;
(ptr as isize).checked_add(byte_offset).unwrap() as *const T
}
offset到开始之前或结束之后都是无定义行为。所以rust认为ptr.offset(i) > ptr,ptr.offset(-i) < ptr,因为如果i能保持在数组的范围之内,则计算的结果是不可能溢出的(这个假设可能是用来做编译优化的?https://users.rust-lang.org/t/understanding-pointer-offset-wrapping-offset-safety/55962/2)。wrapping_offset可以去获取超过范围的地址,Rust也不会做任何假设。
内存管理
一个变量所谓的初始化与未初始化可能在其内存的表示上并无差别,比如一个变量的值被转移后,它就变成未初始化了,但它的内存里的比特位可能还是没变的。变的只是Rust在编译的时候把它当做了已经不再生存了。
Rust在编译的时候做了这样的内存管理,标记那些是生存的那些没有,一些类型也做了类似的管理,比如Vec和Map要记录那些元素是不是生存着。
如果要自己实现类似的类型,也要做相应的内存管理。Rust提供两个操作:
- std::ptr::read(src),src是一个*const T。T是sized,这个操作将src所指的值的所有权转移给调用者。除非T是Copy,否则操作之后应该将src视为未初始化。vec.pop就是以此实现。
- std::ptr::write(dest, value),将value: T转移给dest: *mut T,dest所指的内存必须是一个还未初始化的,T是sized。vec.push就是以此实现
还有一次操作一块内存的:
- std::ptr::copy(src, dst, count),ptr.copy_to(dst, count)
- std::ptr::copy_nonoverlapping(src, dst, count),ptr.copy_to_nonoverlapping(dst, count)
- read_unaligned, write_unaligned
- read_volatile, write_volatile
panic
在unsafe代码中,会暂时出现未定义的行为,但有后面的逻辑马上修正,把整个unsafe的代码变得没有UB,如果这中间panic了会打破这个逻辑造成UB,所以在unsafe代码中要小心在这样的操作中间不要出现可能panic的行为。
union
所有的类型在内存里都是二进制码,而类型的区别全靠对这些二进制的解释。union就是可以选择对其的解释
union SmallOrLarge {
s: bool,
l: u64
}
创建的赋值是安全的,取值是unsafe。因为和enum不一样,union没有tag,也就是没有额外的存储关于类型的信息。因此只有在运行时通过上下文才能知道它的类型。而且也不能保证多种类型之间能有效的互相转换。因此只有在读的时候才危险,因为读的时候才要试图定义它的类型。
按某个类型读的时候并不能确定会从哪位开始读,除非在union的定义上加上一个属性。#[repr(C)]
保证所有的字段都是从第0位开始读。
x86使用little-endian
因为Rust无法知道怎么drop union,所以union的字段都是Copy。
匹配union
每个分支只能用一个字段,且最好写名具体的值,否则永远会匹配成功,假如第一个分支不是写入的字段则会导致UB。
unsafe {
match u {
SmallOrLarge { s: true } => { println!("boolean true"); }
SmallOrLarge { l: 2 } => { println!("integer 2"); }
_ => { println!("something else"); }
}
}
借用union
借用union的一个字段就是借用整个union,借用一个字段作为可变引用,那其他字段都不能再借用,如果借用一个字段作为共享引用,则其他字段也都不能再修改。