深入RUST标准库内核(二 内存)—Layout/原生指针

本书github链接:inside-rust-std-library
前面章节参见:
深入RUST标准库内核(序言) - 简书 (jianshu.com)
深入RUST标准库内核(一 概述) - 简书 (jianshu.com)

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

理解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

ptr模块初探

ptr模块是RUST的对指针的实现模块。相比于C指针内存地址的简单,RUST指针实现机制复杂的多,以满足实现内存安全的类型系统需求,并兼顾内存使用效率和方便性。对内存的操作需要对ptr的若干概念先做一个理解,本节主要基于intrinsics模块的基础对ptr模块的一些类型结构及函数做出分析,为下节的内存类型和函数库做一个基础。

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<Self>][DynMetadata](后面代码解释)
      (例如:DynMetadata<dyn SomeTrait>)
      随着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(..)
            // If returned by `struct_tail_without_normalization` this is a unit struct
            // without any fields, or not a struct, and therefore is Sized.
            | ty::Adt(..)
            // If returned by `struct_tail_without_normalization` this is the empty tuple,
            // a.k.a. unit type, which is Sized
            // 如果是固定类型,元数据是单元类型 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 {
    drop_in_place: fn(*mut ()),
    size_of: usize,
    align_of: usize,
}

PhantomData的含义英文如下:
Zero-sized type used to mark things that "act like" they own a T.
一个零占用的变量,使得结构拥有了一个T类型的变量。在RUST中,这一用法常常为了表示生命周期关系,以作为安全性的一个判断。

VTable 中包含4个成员,上面的结构体仅列出了前三个,即指向实现Trait的结构的drop_in_place函数的指针; 结构内存占用字节大小;结构内存对齐字节大小;VTable结构后面的内存为Trait的所有行为的函数指针数组。

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安全性的一个体现。
ptr::null_mut<T>()->*mut T 同上,只是返回的是*mut T
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] 由原生指针类型及切片长度获得原生切片类型指针

ptr模块的函数大部分逻辑都比较简单。很多就是对intrinsic 函数做调用。*const T/* mut T被使用的场景如下:
1.需要做内存布局相同的两个类型之间的转换,
2.对于数组或切片做头指针偏移以获取元素变量
3.由内存头指针生成数组或切片指针
4.内存拷贝或内存读出/写入
以上4个场景实际上都是编程中最基础的操作。

* const T生成*const [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 {
    // SAFETY: Accessing the value from the `PtrRepr` union is safe since *const T
    // and PtrComponents<T> have the same memory layouts. Only std can make this
    // guarantee.
    //由以下这个操作可以确认 * const T实质是个结构体。
    unsafe { PtrRepr { components: PtrComponents { data_address, metadata } }.const_ptr }
}

*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函数主要完成不同类型的原生指针的互相转换,这里需要程序员确保U与T的内存布局一致,并保证指针的元数据也一致。
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的方法的逻辑基本也都比较简单,但涉及到较多的指针类型转换,有时需要细致分析,举例如下:

    //该方法给* mut T置一个新值    
    pub fn set_ptr_value(mut self, val: *const u8) -> Self {
        // 指针类型分析如下
        // self: * mut T
        // &mut self:&mut *mut T
        // &mut self as *mut *const T: *mut *mut T as *mut *const T
        // &mut self as *mut *const T as *mut *const u8: *mut *const T as * mut *const u8
        let thin = &mut self as *mut *const T as *mut *const u8;
        // 指针类型分析如下
        //*thin: *(*mut *const u8)即mut *const u8  
        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
   }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容