unsafe和无定义行为

普通的代码就像rust和你签的契约,你遵守契约,rust保证你的安全,包括内存安全和没有无定义的行为。unsafe代码就是你告诉rust我在这里不像遵守契约,同时我也会承担其带来的责任。

unsafe代码块

unsafe代码块可以解锁五个功能:

  1. 可以调用unsafe函数。
  2. 可以使用raw pointer,普通代码只能创建和传递raw pointer。
  3. 可以访问union的字段。
  4. 可以使用可变的static 变量。
  5. 可以使用ffi。

是有unsafe代码块提议提醒写的人真的需要这样吗?也提醒review代码的人这里要注意。

unsafe代码块所造成的影响不一定只发生在代码块里,有可能是因为从代码块之前获取的变量所造成,返回的值也可能对后面的代码带来影响。

选择用unsafe代码块还是unsafe函数:如果使用该函数可以通过编译但还是有可能造成无定义后果的话,必须把函数定义成unsafe。

无定义行为

无定义行为(undefined behavior):Rust很确定的认为你的程序不会展现的行为。
编译器的工作是把一种语言编译成另外一种语言,如果翻译前后两种语言的行为一样。就认为两个程序是一样的。

  1. fn very_trustworthy(shared: &i32) {
  2. unsafe {
  3. // Turn the shared reference into a mutable pointer.
  4. // This is undefined behavior.
  5. let mutable = shared as *const i32 as *mut i32;
  6. *mutable = 20;
  7. }
  8. }
  9. fn main() {
  10. let i = 10;
  11. very_trustworthy(&i);
  12. println!("{}", i * 100);
  13. }

如果只看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。

  1. use core::nonzero::Zeroable;
  2. fn zeroed_vector<T>(len: usize) -> Vec<T>
  3. where T: Zeroable
  4. {
  5. let mut vec = Vec::with_capacity(len);
  6. unsafe {
  7. std::ptr::write_bytes(vec.as_mut_ptr(), 0, len);
  8. vec.set_len(len);
  9. }
  10. vec
  11. }
  12. et v: Vec<usize> = zeroed_vector(100_000);
  13. 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。通过*取值。

  1. let mut x = 10;
  2. let ptr_x = &mut x as *mut i32;
  3. let y = Box::new(20);
  4. let ptr_y = &*y as *const i32;
  5. unsafe {
  6. *ptr_x += *ptr_y;
  7. }
  8. assert_eq!(x, 30);

和Rust的其他指针不同,raw pointer可以为null。

  1. fn option_to_raw<T>(opt: Option<&T>) -> *const T {
  2. match opt {
  3. None => std::ptr::null(),
  4. Some(r) => r as *const T
  5. }
  6. }
  7. assert!(!option_to_raw(Some(&("pea", "pod"))).is_null());
  8. 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等。

  1. let trucks = vec!["garbage truck", "dump truck", "moonstruck"];
  2. let first: *const &str = &trucks[0];
  3. let last: *const &str = &trucks[2];
  4. assert_eq!(unsafe { last.offset_from(first) }, 2);
  5. assert_eq!(unsafe { first.offset_from(last) }, -2);

生成raw pointer时只要写明类型,rust会自动从普通引用转换,但反过来不行。也就是说从安全到不安全很容易,从不安全去安全有限制。
在转换的时候可能需要分步

  1. &vec![42_u8] as *const String; // error: invalid conversion
  2. &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。

  1. mod ref_with_flag {
  2. use std::marker::PhantomData;
  3. use std::mem::align_of;
  4. /// A `&T` and a `bool`, wrapped up in a single word.
  5. /// The type `T` must require at least two-byte alignment.
  6. ///
  7. /// If you're the kind of programmer who's never met a pointer whose
  8. /// 2⁰-bit you didn't want to steal, well, now you can do it safely!
  9. /// ("But it's not nearly as exciting this way...")
  10. pub struct RefWithFlag<'a, T> {
  11. ptr_and_bit: usize,
  12. behaves_like: PhantomData<&'a T> // occupies no space
  13. }
  14. impl<'a, T: 'a> RefWithFlag<'a, T> {
  15. pub fn new(ptr: &'a T, flag: bool) -> RefWithFlag<T> {
  16. assert!(align_of::<T>() % 2 == 0);
  17. RefWithFlag {
  18. ptr_and_bit: ptr as *const T as usize | flag as usize,
  19. behaves_like: PhantomData
  20. }
  21. }
  22. pub fn get_ref(&self) -> &'a T {
  23. unsafe {
  24. let ptr = (self.ptr_and_bit & !1) as *const T;
  25. &*ptr
  26. }
  27. }
  28. pub fn get_flag(&self) -> bool {
  29. self.ptr_and_bit & 1 != 0
  30. }
  31. }
  32. }

因为从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返回一个类型为const T的null pointer,std::ptr::null_mut返回一个类型为mut T的null pointer。

可以用is_null方法判断是否为空指针。as_ref 和as_mut 更方便。

内存大小和对齐

std::mem::size_of::()返回类型T的大小,std::mem::align_of::()返回T的对齐。
对齐必须是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+(i
size)获得。offset方法基本就是这样的:

  1. fn offset<T>(ptr: *const T, count: isize) -> *const T
  2. where T: Sized
  3. {
  4. let bytes_per_element = std::mem::size_of::<T>() as isize;
  5. let byte_offset = count * bytes_per_element;
  6. (ptr as isize).checked_add(byte_offset).unwrap() as *const T
  7. }

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

例子GapBuffer

panic

在unsafe代码中,会暂时出现未定义的行为,但有后面的逻辑马上修正,把整个unsafe的代码变得没有UB,如果这中间panic了会打破这个逻辑造成UB,所以在unsafe代码中要小心在这样的操作中间不要出现可能panic的行为。

union

所有的类型在内存里都是二进制码,而类型的区别全靠对这些二进制的解释。union就是可以选择对其的解释

  1. union SmallOrLarge {
  2. s: bool,
  3. l: u64
  4. }

创建的赋值是安全的,取值是unsafe。因为和enum不一样,union没有tag,也就是没有额外的存储关于类型的信息。因此只有在运行时通过上下文才能知道它的类型。而且也不能保证多种类型之间能有效的互相转换。因此只有在读的时候才危险,因为读的时候才要试图定义它的类型。

按某个类型读的时候并不能确定会从哪位开始读,除非在union的定义上加上一个属性。#[repr(C)]保证所有的字段都是从第0位开始读。

x86使用little-endian

因为Rust无法知道怎么drop union,所以union的字段都是Copy。

匹配union

每个分支只能用一个字段,且最好写名具体的值,否则永远会匹配成功,假如第一个分支不是写入的字段则会导致UB。

  1. unsafe {
  2. match u {
  3. SmallOrLarge { s: true } => { println!("boolean true"); }
  4. SmallOrLarge { l: 2 } => { println!("integer 2"); }
  5. _ => { println!("something else"); }
  6. }
  7. }

借用union

借用union的一个字段就是借用整个union,借用一个字段作为可变引用,那其他字段都不能再借用,如果借用一个字段作为共享引用,则其他字段也都不能再修改。