Rust的内存模型

TL;DR

最近在尝试rCore这个内核实验

这本教程旨在一步一步展示如何 从零开始 用 Rust 语言写一个基于 RISC-V 架构的 类 Unix 内核 。

所以只是理解Rust的各种语言特性就不够了,需要学习Rust的内存模型,理解它是如何在内存空间中组织各个实例


基于C++的堆栈管理

Rust对于数据存放在栈还是堆上有着清晰的界定

只存放编译器确定的数据

存放大小动态变化或需要在作用域之外继续生存的数据

  • 基础类型:bool, i32, i64等,存放在栈上

  • 结构体:默认存放在栈上(Rust中结构体会经历重排优化

  • &str:本质是一个指针和长度信息的集合

  • Box,String: 在堆上分配


RAII and Drop

RAII

上的生命周期和C/C++中是一样的,在所在栈帧结束后,统一将栈上的实例pop出来

上的内存由栈上的引用/指针管理,在栈数据弹出之前,会自动调用该变量的Drop函数,释放内存 这就是基本的RAII思想

堆栈之间的转换与移动

当把一个包含堆数据的变量赋值给另一个变量时,Rust 会执行浅拷贝,仅复制栈上的指针和元数据

这避免了额外的内存拷贝和双重释放(Double Free)


更深入一些

让我们来看真实的反汇编代码

为了方便我们看汇编,所以需要在.cargo/config.toml(没有则创建)中声明不优化代码

这样编译器生成的汇编就会在栈/堆上显式分配内存

[profile.dev]
opt-level = 1

找到fn main

在Rust中,使用gdb直接b main找到的确实是main函数,但并不是业务逻辑的main函数,而是Rust初始化运行时环境的代码

如果使用objdump或gdb中直接反汇编main函数,只会看到

0000000000009680 <main>:
    9680: 50                           	push	rax
    9681: 48 89 f1                     	mov	rcx, rsi
    9684: 48 63 d7                     	movsxd	rdx, edi
    9687: 48 8d 05 d2 ff ff ff         	lea	rax, [rip - 0x2e]       # 0x9660 <_RNvCseoItouaaNcv_8rust_lab4main>
    968e: 48 89 04 24                  	mov	qword ptr [rsp], rax
    9692: 48 8d 35 e7 04 06 00         	lea	rsi, [rip + 0x604e7]    # 0x69b80 <stderr+0x8>
    9699: 48 89 e7                     	mov	rdi, rsp
    969c: 45 31 c0                     	xor	r8d, r8d
    969f: ff 15 33 28 06 00            	call	qword ptr [rip + 0x62833] # 0x6bed8 <_GLOBAL_OFFSET_TABLE_+0x6a8>
    96a5: 59                           	pop	rcx
    96a6: c3                           	ret
    96a7: 66 0f 1f 84 00 00 00 00 00   	nop	word ptr [rax + rax]

代码中的fn main() {}则是在_RNvCseoItouaaNcv_8rust_lab4main这个标签中

具体的标准在Rust RFC Book

简单来讲,这个是基于C++ g++ v3的风格进行的符号修饰

Instance

use std::hint::black_box;

#[inline(never)]
fn process_buffer() {
    let mut data = vec![0u8; 1024];
    data[0] = 42;
    black_box(&data); // 阻断优化
}

fn main() {
    process_buffer();
}

下面是对应的process_buffer()的反汇编

00000000000136f0 <_RNvCs31H03Ek5cJ6_8rust_lab14process_buffer>:
   136f0: 48 83 ec 28                  	sub	rsp, 0x28
   136f4: ff 15 c6 f4 03 00            	call	qword ptr [rip + 0x3f4c6] # 0x52bc0 <writev+0x52bc0>
   136fa: bf 00 04 00 00               	mov	edi, 0x400
   136ff: be 01 00 00 00               	mov	esi, 0x1
                              // 调用内存分配器分配堆内存
   13704: ff 15 be f4 03 00            	call	qword ptr [rip + 0x3f4be] # 0x52bc8 <writev+0x52bc8>
                              // 检查返回是否为空指针(OOM处理)
   1370a: 48 85 c0                     	test	rax, rax
   1370d: 74 48                        	je	0x13757 <_RNvCs31H03Ek5cJ6_8rust_lab14process_buffer+0x67>
                              // 构建Vec
   1370f: 48 c7 44 24 08 00 04 00 00   	mov	qword ptr [rsp + 0x8], 0x400
   13718: 48 89 44 24 10               	mov	qword ptr [rsp + 0x10], rax
   1371d: 48 c7 44 24 18 00 04 00 00   	mov	qword ptr [rsp + 0x18], 0x400
   13726: c6 00 2a                     	mov	byte ptr [rax], 0x2a
   13729: 48 8d 44 24 08               	lea	rax, [rsp + 0x8]
   1372e: 48 89 44 24 20               	mov	qword ptr [rsp + 0x20], rax
   13733: 48 8d 44 24 20               	lea	rax, [rsp + 0x20]
                              // 离开作用域,自动Drop
   13738: 48 8b 74 24 08               	mov	rsi, qword ptr [rsp + 0x8]
   1373d: 48 85 f6                     	test	rsi, rsi
   13740: 74 10                        	je	0x13752 <_RNvCs31H03Ek5cJ6_8rust_lab14process_buffer+0x62>
   13742: 48 8b 7c 24 10               	mov	rdi, qword ptr [rsp + 0x10]
   13747: ba 01 00 00 00               	mov	edx, 0x1
                              // 调用dealloc函数
   1374c: ff 15 7e f4 03 00            	call	qword ptr [rip + 0x3f47e] # 0x52bd0 <writev+0x52bd0>
   13752: 48 83 c4 28                  	add	rsp, 0x28
   13756: c3                           	ret

可以观察到

  • Vec这样到指针在底层上,存储了三元组(Capacity, Pointer, Length),实际上就是俗称的胖指针概念

这里推荐一个blog

  • Drop的时候,先取出了胖指针中的capacity,检测是否为空,再进行释放

End

写这篇东西的初衷是 rCore。做内核实验的时候,光是背下所有权借用的规则是不够的,还得知道这些规则在内存里是怎么落实的:哪些数据在栈上,哪些在堆上,作用域结束时Drop又是怎么被触发的。

回头看一下,收获最大的反而是最后那段反汇编。与其靠文档脑补BoxString这类类型的行为,不如直接看生成的指令。看到Vec在底层就是一个(Capacity, Pointer, Length)三元组,看到Drop先取容量判断非空再调用dealloc,内存模型就不再是纸面上的概念了。

如果后面还有时间,想沿着这条思路继续往下挖。比如所有权转移在汇编层面到底发生了什么,或者进入no_std之后全局分配器是怎么接进内核的。那才是 rCore 真正要碰的东西。

recent-work

Rust的内存模型

TL;DR 最近在尝试rCore这个内核实验 这本教程旨在一步一步展示如何 从零开始 用 Rust 语言写一个基于 RISC-V 架构的 类 Unix 内核 。 所以只是理解Rust的各种语言特性就不够了,需要学习Rust的内存模型,理解它是如何在内存空间中组织各个实例 基于 …

Read more →

数字电路知识点记录

TL;DR 最近太无聊,重新拾起cs61c课程,开始有些痛苦的数字电路学习。 我的仓库 这个博客就用来记录一下各种硬件的引脚和电路设计等知识。 Warm up Basic 在更高抽象的程序中,一般情况下都是串行的。 但是在硬件视角中,都是并行的。(电路很快,所以要有负反馈/回授等 …

Read more →