深入RUST标准库内核—内存之原生指针

Rust
483
0
0
2022-09-05

本文摘自 inside-rust-std-library

RUST标准库内存模块代码分析

内存模块的代码路径举例如下(以作者电脑上的路径):

%USER%.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\alloc*.*

%USER%.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\ptr*.*

%USER%.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\mem*.*

%USER%.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\intrinsic.rs

%USER%.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\alloc\src\alloc.rs

RUST之所以被认为难学,是因为RUST与C相同,需要对内存做彻底的控制,即程序可以在代码中编写专属内存管理系统,并将内存管理系统与语言类型相关联,将内存块与语言类型做自如的转换。对于除了C之外的高级语言,内存管理是编译器的任务,这就导致对于RUST的内存管理缺乏经验。相对于C,RUST的现代语法特性及内存安全导致RUST的内存块与类型系统的转换不容易被理解。本节将从标准库的内存模块的代码分析中给出RUST内存的本质。理解了RUST内存,RUST语言的最难关便过了。

RUST类型系统的内存布局

类型内存布局是指类型的内部变量在内存布局中,内存顺序,内存大小,内存字节对齐等内容。

对于GC机制的高级语言,类型内存布局一般是交由编译器决定的。程序员不需要关心。C/C++语言中类型只有固定的一种内存布局排序方式和一经编译参数配置即固定的对齐方式,编译器不会对此进行优化,类型内存布局对程序员是透明的。

RUST则不同,因为泛型,闭包,编译器优化的关系,类型内存布局方式编译器会根据需要对内存布局做调整,对程序员来说复杂类型的内存布局是完全不可预测的,而在内存操作中,类型内存布局的一些信息是必须要使用的,所以,RUST提供了Layout内存布局类型。此布局类型结构是类型内存操作的基础。

Layout的数据结构如下:

pub struct Layout {

    // size of the requested block of memory, measured in bytes.

    // 类型需占用的内存大小,用字节数目表示

    size_: usize,

    //  按照此字节数目进行类型内存对齐, NonZeroUsize见代码后面文字分析

    align_: NonZeroUsize,

}

NonZeroUsize是一种非0值的usize, 这种类型主要应用于不可取0的值,本结构中, 字节对齐属性变量不能被置0,所以用NonZeroUsize来确保安全性。如果用usize类型,那代码中就可能会把0置给align_,导致bug产生。这是RUST的一个设计规则,所有的限制要在类型定义即显性化,从而使bug在编译中就被发现。

每一个RUST的类型都有自身独特的内存布局Layout。一种类型的Layout可以用intrinsic::<T>::size_of()intrinsic::<T>::min_align_of()获得的类型内存大小和对齐来获得。

RUST的内存布局更详细原理阐述请参考[RUST内存布局] (https://doc.rust-lang.org/nomicon/data.html),

#[repr(transparent)]内存布局模式

repr(transparent)用于仅包含一个成员变量的类型,该类型的内存布局与成员变量类型的内存布局完全一致。类型仅仅具备编译阶段的意义,在运行时,类型变量与其成员变量可以认为是一个相同变量,可以相互无障碍类型转换。使用repr(transparent)布局的类型基本是一种封装结构。

#[repr(packed)]内存布局模式

强制类型成员变量以1字节对齐,此种结构在协议分析和结构化二进制数据文件中经常使用

#[repr(RUST)]内存布局模式

默认的布局方式,采用此种布局,RUST编译器会根据情况来自行优化内存

#[repr(C)]内存布局模式

采用C语言布局方式, 所有结构变量按照声明的顺序在内存排列。默认4字节对齐。

RUST内存的类型与函数库体系

intrinsic 固有函数库——内存部分

intrinsics函数由编译器内置实现,并提供给其他模块使用,对于固有函数,没必要去关注如何实现,重要的是了解其功能和如何使用,intrinsics内存函数一般不由库以外的代码直接调用,而是由mem模块和ptr模块封装后再提供给其他模块。

intrinsics::drop_in_place<T:Sized?>(to_drop: * mut T) 在编译器无法自动drop时, 手工调用此函数将内存释放

intrinsics::forget<T:Sized?> (_:T), 通知编译器不回收forget的变量内存

intrinsics::needs_drop<T>()->bool, 判断T类型是否需要做drop操作,实现了Copy Trait的类型会返回false

intrinsics::transmute<T,U>(e:T)->U, 对于内存布局相同的类型 T和U, 完成将类型T变量转换为类型U变量

intrinsics::offset<T>(dst: *const T, offset: usize)->* const T, 相当于C的类型指针加减计算

intrinsics::copy<T>(src:*const T, dst: *mut T, count:usize), 内存拷贝, src和dst内存可重叠, 类似c语言中的memmove

intrinsics::copy_no_overlapping<T>(src:*const T, dst: * mut T, count:usize), 内存拷贝, src和dst内存不重叠

intrinsics::write_bytes(dst: *mut T, val:u8, count:usize) , C语言的memset的RUST实现

intrinsics::size_of<T>()->usize 类型内存空间字节大小

intrinsics::min_align_of<T>()->usize 返回类型对齐字节大小

intrinsics::size_of_val<T>(_:*const T)->usize返回指针指向的变量内存空间字节大小

intrinsics::min_align_of_val<T>(_: * const T)->usize 返回指针指向的变量对齐字节大小

intrinsics::volatile_xxxx 通知编译器不做内存优化的操作函数,一般用于硬件访问

intrinsics::volatile_copy_nonoverlapping_memory<T>(dst: *mut T, src: *const T, count: usize) 内存拷贝

intrinsics::volatile_copy_memory<T>(dst: *mut T, src: *const T, count: usize) 功能类似C语言memmove

intrinsics::volatile_set_memory<T>(dst: *mut T, val: u8, count: usize) 功能类似C语言memset

intrinsics::volatile_load<T>(src: *const T) -> T读取内存或寄存器,字节对齐

intrinsics::volatile_store<T>(dst: *mut T, val: T)内存或寄存器写入,字节对齐

intrinsics::unaligned_volatile_load<T>(src: *const T) -> T 字节非对齐

intrinsics::unaligned_volatile_store<T>(dst: *mut T, val: T)字节非对齐

intrinsics::raw_eq<T>(a: &T, b: &T) -> bool 内存比较,类似C语言memcmp

pub fn ptr_offset_from<T>(ptr: *const T, base: *const T) -> isize 基于类型T内存布局的偏移量

pub fn ptr_guaranteed_eq<T>(ptr: *const T, other: *const T) -> bool 判断两个指针是否判断, 相等返回ture, 不等返回false

pub fn ptr_guaranteed_ne<T>(ptr: *const T, other: *const T) -> bool 判断两个指针是否不等,不等返回true

后继章节还会对intrinsic的其他函数做简单分析。intrinsic的详细及全面的内容请参考RUST官方的标准库指南

ptr模块初探

ptr模块是RUST的对指针的实现模块。

ptr模块主要包含原生指针*const T/*mut T, 原生指针封装类型NonNull<T>/Unique<T>, 本节主要集中在原生指针的类型结构及少量方法,为后继的mem模块分析做准备。

RUST原生指针不象C那样仅仅是一个地址值,为满足实现内存安全的类型系统需求,并兼顾内存使用效率和方便性,RUST的指针实质是一个不太容易理解的类型结构。

ptr模块中原生指针具体实现

RUST的原生指针类型(*const T/*mut T)实质是个数据结构体,由两个部分组成,第一个部分是一个内存地址,第二个部分对这个内存地址的限制性描述-元数据

//从下面结构定义可以看到,*const T本质就是PtrComponents<T>
pub(crate) union PtrRepr<T: ?Sized> {
    pub(crate) const_ptr: *const T,
    pub(crate) mut_ptr: *mut T,
    pub(crate) components: PtrComponents<T>,
}

pub(crate) struct PtrComponents<T: ?Sized> {
    //只能用*const (), * const T编译器已经默认还带有元数据。 
    pub(crate) data_address: *const (),
    //不同类型指针的元数据 
    pub(crate) metadata: <T as Pointee>::Metadata,
}

//从下面Pointee的定义可以看到一个RUST的编程技巧,即Trait可以只用来实现对关联类型的指定,Pointee这一Trait即只用来指定Metadata的类型。
pub trait Pointee {
    /// The type for metadata in pointers and references to `Self`. 
    type Metadata: Copy + Send + Sync + Ord + Hash + Unpin;
}

//廋指针元数据是单元类型,即是空
pub trait Thin = Pointee<Metadata = ()>;

元数据的规则:

  • 对于固定大小类型的指针(实现了 Sized Trait), RUST定义为廋指针(thin pointer),元数据大小为0,类型为(),这里要注意,RUST中数组也是固定大小的类型,运行中对数组下标合法性的检测,就是比较是否已经越过了数组的内存大小。
  • 对于动态大小类型的指针(DST 类型),RUST定义为胖指针(fat pointer 或 wide pointer), 元数据为:
  • 对于结构类型,如果最后一个成员是动态大小类型(结构的其他成员不允许为动态大小类型),则元数据为此动态大小类型
  • 的元数据
  • 对于str类型, 元数据是按字节计算的长度值,元数据类型是usize
  • 对于切片类型,例如[T]类型,元数据是数组元素的数目值,元数据类型是usize
  • 对于trait对象,例如 dyn SomeTrait, 元数据是 [DynMetadata][DynMetadata](后面代码解释)
  • (例如:DynMetadata)

随着RUST的发展,有可能会根据需要引入新的元数据种类。

在标准库代码当中没有指针类型如何实现Pointee Trait的代码,编译器针对每个类型自动的实现了Pointee。

如下为rust编译器代码的一个摘录

    pub fn ptr_metadata_ty(&'tcx self, tcx: TyCtxt<'tcx>) -> Ty<'tcx> {
        // FIXME: should this normalize? 
        let tail = tcx.struct_tail_without_normalization(self);
        match tail.kind() {
            // Sized types
            ty::Infer(ty::IntVar(_) | ty::FloatVar(_))
            | ty::Uint(_)
            | ty::Int(_)
            | ty::Bool
            | ty::Float(_)
            | ty::FnDef(..)
            | ty::FnPtr(_)
            | ty::RawPtr(..)
            | ty::Char
            | ty::Ref(..)
            | ty::Generator(..)
            | ty::GeneratorWitness(..)
            | ty::Array(..)
            | ty::Closure(..)
            | ty::Never
            | ty::Error(_)
            | ty::Foreign(..)
            | ty::Adt(..)
            // 如果是固定类型,元数据是单元类型 tcx.types.unit,即为空
            | ty::Tuple(..) => tcx.types.unit,
            //对于字符串和切片类型,元数据为长度tcx.types.usize,是元素长度
            ty::Str | ty::Slice(_) => tcx.types.usize,
            //对于dyn Trait类型, 元数据从具体的DynMetadata获取*
            ty::Dynamic(..) => {
                let dyn_metadata = tcx.lang_items().dyn_metadata().unwrap();
                tcx.type_of(dyn_metadata).subst(tcx, &[tail.into()])
            },
            //以下类型不应有元数据
            ty::Projection(_)
            | ty::Param(_)
            | ty::Opaque(..)
            | ty::Infer(ty::TyVar(_))
            | ty::Bound(..)
            | ty::Placeholder(..)
            | ty::Infer(ty::FreshTy(_) | ty::FreshIntTy(_) | ty::FreshFloatTy(_)) => {
                bug!("`ptr_metadata_ty` applied to unexpected type: {:?}", tail)
            }
        }
    }

以上代码中的中文注释比较清晰的说明了编译器对每一个类型(或类型指针)都实现了Pointee中元数据类型的获取。

对于Trait对象的元数据的具体结构定义见如下代码:

//dyn Trait的元数据结构
pub struct DynMetadata<Dyn: ?Sized> {
    //堆中的函数VTTable变量的指针
    vtable_ptr: &'static VTable,
    //标示结构对Dyn的所有权关系
    phantom: crate::marker::PhantomData<Dyn>,
}

struct VTable {
    //指向实现Trait的类型结构体的drop_in_place函数的指针
    drop_in_place: fn(*mut ()),
    //指向实现Trait的类型结构体的大小
    size_of: usize,
    //指向实现Trait的类型结构体字节对齐
    align_of: usize,
    //后继是结构体实现Trait的所有方法的指针数组
}

PhantomData的含义英文如下:

Zero-sized type used to mark things that “act like” they own a T.

一个零占用的变量,使得结构逻辑上拥有了一个T类型的变量的所有权,编译器即可用PhantomData判断类型结构体的一些代码安全问题。

ptr模块函数

ptr::drop_in_place<T: ?Sized>(to_drop: *mut T) 此函数是编译器实现的,用于不需要RUST自动drop时,由程序代码调用以释放内存

ptr::metadata<T: ?Sized>(ptr: *const T) -> <T as Pointee>::Metadata用来返回原生指针的元数据

ptr::null<T>() -> *const T 返回0值的*const T,因为RUST安全代码中指针不可能为0,所以只能用这个函数获得0值的* const T,这个函数也是RUST安全性的一个体现。需要说明,此时* const T仅具有数值意义。

ptr::null_mut<T>()->*mut T 同上,只是返回的是*mut T

ptr::eq<T>(a: *const T, b: *const T)->bool 比较指针,此处需要注意,地址比较不但是地址,也比较元数据

ptr::from_raw_parts<T: ?Sized>(data_address: *const (), metadata: <T as Pointee>::Metadata) -> *const T 从内存地址和元数据生成原生指针

ptr::from_raw_parts_mut<T: ?Sized>(data_address: *mut (), metadata: <T as Pointee>::Metadata) -> *mut T 功能同上,形成可变指针

RUST指针类型转换时,经常使用以上两个函数获得需要的指针类型。

ptr::slice_from_raw_parts<T>(data: *const T, len: usize) -> *const [T]

ptr::slice_from_raw_parts_mut<T>(data: *mut T, len: usize) -> *mut [T] 由原生指针类型及切片长度获得原生切片类型指针

代码如下:

pub const fn slice_from_raw_parts<T>(data: *const T, len: usize) -> *const [T] {
    //data.cast()将*const T转换为 *const() 
    from_raw_parts(data.cast(), len)
}

pub const fn from_raw_parts<T: ?Sized>(
    data_address: *const (),
    metadata: <T as Pointee>::Metadata,
) -> *const T {
    //由以下代码可以确认 * const T实质就是PtrRepr类型结构体。 
    unsafe { PtrRepr { components: PtrComponents { data_address, metadata } }.const_ptr }
}

ptr模块的函数大部分逻辑都比较简单。很多就是对intrinsic 函数做调用。

*const T/* mut T被使用的场景如下:

1.需要做内存布局相同的两个类型之间的转换,

2.对于数组或切片做头指针偏移以获取元素变量

3.由内存头指针生成数组或切片指针

4.内存拷贝或内存读出/写入

以上4个场景实际上都是底层编程中最基础的操作。

*const T/*mut T/*const [T]/*mut [T] 若干方法

ptr::*const T::is_null(self)->bool

ptr::*mut T::is_null(self)->bool此函数判断原生指针的地址值是否为0

ptr::*const T::cast<U>(self) -> *const U ,本质上就是一个*const T as *const U

ptr::*mut T::cast<U>(self)->*mut U 连同上行,cast函数主要完成不同类型的原生指针的互相转换,要注意,如果后继要把返回的指针转换成引用,那必须保证T类型与U类型内存布局完全一致。如果仅仅是将返回值做数值应用,则此限制可以不遵守。

ptr::*const T::to_raw_parts(self) -> (*const (), <T as super::Pointee>::Metadata)

ptr::*mut T::to_raw_parts(self)->(* const (), <T as super::Pointee>::Metadata) 由原生指针获得地址及元数据

ptr::*const T::as_ref<`a>(self) -> Option<&`a T> 将原生指针转换为引用,因为*const T可能为零,所有需要转换为Option<& `a T>类型,转换的安全性由程序员保证,尤其注意满足RUST对引用的安全要求。转换后,数据进入安全的RUST环境。

ptr::*mut T::as_ref<`a>(self)->Option<&`a T> 同上

ptr::*mut T::as_mut<`a>(self)->Option<&`a mut T>同上,但转化类型为 &mut T。

ptr::*const T::offset(self, count:isize)->* const T *mut T::offset(self, count:isize)->* mut T 实质是intrinsics::offset的封装

ptr::*const [T]::len()->usize 获取切片元素数量

*const T*mut T的方法的逻辑基本也都比较简单,难点在于涉及到较多的指针类型转换,有时需要细致分析,举例如下:

    //该方法用于仅给指针的 address部分赋值   
    pub fn set_ptr_value(mut self, val: *const u8) -> Self {
        // 这个类型变换几乎是指针变换最多的代码 
        // 将self转换为一个瘦指针, self原先可能是一个胖指针 
        let thin = &mut self as *mut *const T as *mut *const u8;
        // 这个赋值仅仅做了address的赋值,对于瘦指针,这个相当于赋值操作, 
        // 对于胖指针,则没有改变胖指针的元数据。这种操作方式仅仅在极少数的情况下 
        // 可以使用,极度危险。 
        unsafe { *thin = val };
        self
    }

RUST引用&T的安全要求

  1. 引用的内存地址必须是内存2的幂次字节对齐的
  2. 引用的内存内容必须是初始化过的

举例:

#[repr(packed)] 
    struct RefTest {a:u8, b:u16, c:u32}
    fn main() {
        let test = RefTest{a:1, b:2, c:3};
        //下面代码无法通过编译,因为test.b 内存字节位于奇数,无法用于借用 
        let ref1 = &test.b
    }

NonNull

定义如下:

#[repr(transparent)]
pub struct NonNull<T: ?Sized> {
    pointer: *const T,
}

如前文,因为NonNull<T>repr(transparent)内存布局模式,所以,NonNull<T>在内存中与*const T完全一致。可以直接转化为* const T

RUST常用的这种封装类型。因为* const T是没有所有权,借用,生命周期这些属性的,完全脱离了RUST编译器的管理。但用NonNull封装后,NonNull作为一个类型变量,自身具备了所有权,借用,生命周期这些属性, RUST编译器就可以对其管理了。此时要注意区别NonNull变量自身具有的所有权与NonNull拥有内部变量pointer指向的*const T的所有权的区别。NonNull不拥有*const T的所有权

另外,NonNull确保指针是非零值。指针为0实际上代表指针异常,在RUST中,这种情况应该用Option::None来表示。有了NonNull后,即可以用Option<NonNull>来处理指针为0的情况。

NonNull的相关方法:

NonNull::<T>::cast<U>(self)->NonNull<U> 指针类型转换,程序员应该保证T和U的内存布局相同

    pub const fn cast<U>(self) -> NonNull<U> {
        //用内部变量指针类型转换完成转换 
        unsafe { NonNull::new_unchecked(self.as_ptr() as *mut U) }
    }

NonNull::<T>::new(* mut T)->Option<Self> 可以判断* mut T是否为0值,并返回Option<NonNull<T>>类型,

    pub fn new(ptr: *mut T) -> Option<Self> {
        if !ptr.is_null() {
            Some(unsafe { Self::new_unchecked(ptr) })
        } else {
            None
        }
    }

NonNull::<T>::new_unchecked(* mut T)->Self 封装* mut T,不检查* mut T是否为0,调用者应保证* mu T不为0

    pub const unsafe fn new_unchecked(ptr: *mut T) -> Self {
        unsafe { NonNull { pointer: ptr as _ } }
    }

NonNull::<T>::from_raw_parts( data_address: NonNull<()>, metadata: <T as super::Pointee>::Metadata) -> NonNull<T>,实际上先调用ptr::from_raw_parts形成* const T的指针,然后再形成NonNull<T>

    pub const fn from_raw_parts(
        data_address: NonNull<()>,
        metadata: <T as super::Pointee>::Metadata,
    ) -> NonNull<T> {
        // 会调用 `ptr::from_raw_parts_mut`. 
        unsafe {
            NonNull::new_unchecked(super::from_raw_parts_mut(data_address.as_ptr(), metadata))
        }
    }

NonNull::<T>::to_raw_parts(self) -> (NonNull<()>, <T as super::Pointee>::Metadata) 上面函数的反操作

NonNull::<T>::as_ptr(self)->* mut T 返回原生指针

    pub const fn as_ptr(self) -> *mut T {
        self.pointer as *mut T
    }

NonNull::<T>::as_ref<`a>(&self)->&`a T

    pub unsafe fn as_ref<'a>(&self) -> &'a T {
        // 常用的代码 
        unsafe { &*self.as_ptr() }
    }

NonNull::<T>::as_mut<`a>(&mut self)->&`a mut T 从NonNull返回结构引用,此时程序员应保证符合引用安全的规则

NonNull::<[T]>::slice_from_raw_parts(data: NonNull<T>, len: usize) -> Self 将类型指针转化为类型的切片类型指针,实质是ptr::slice_from_raw_parts的一种包装。

以下的实例展示了 NonNull在动态申请堆内存的使用:

    impl Global {
        fn alloc_impl(&self, layout: Layout, zeroed: bool) -> Result<NonNull<[u8]>, AllocError> {
            match layout.size() {
                0 => Ok(NonNull::slice_from_raw_parts(layout.dangling(), 0)),
                // SAFETY: `layout` is non-zero in size,
                size => unsafe {
                    //raw_ptr是 *const u8类型 
                    let raw_ptr = if zeroed { alloc_zeroed(layout) } else { alloc(layout) };
                    //NonNull::new处理了raw_ptr为零的情况,返回NonNull<u8>,此时内存长度还与T不匹配 
                    let ptr = NonNull::new(raw_ptr).ok_or(AllocError)?;
                    //将NonNull<u8>转换为NonNull<[u8]>, NonNull<[u8]>已经是类型T的内存长度。后继可以直接转换为T类型的指针了。 
                    Ok(NonNull::slice_from_raw_parts(ptr, size))
                },
            }
        }
        ....
    }