Rust 中调用 Drop 的时机
2024-09-06 07:05 rust
Drop
trait 在好些地方都有所提及, 但是它们的重点不太一样, 比如前文有介绍
Drop trait 的基本用法, 以及 所有权转移.
在这一节中, 我们重点介绍 Drop
trait 被调用的时机.
谁负责调用 Drop trait
编译器, 确且地说是编译器自动生成的汇编代码, 帮我们自动管理对像的释放, 通过调用 Drop
trait.
就像在 C++ 语言中, 编译器会自动调用对象的析构函数.
但是, 跟 C++ 相比, Rust 管理对象的释放过程要复杂得多, 后者的对象会有 未初始化 uninit
的状态,
如果处于这个状态, 那么编译器就不会调用该对象的 Drop
trait.
静态释放 static drop
表达式比较简单, 可以在编译期间确定变量的值是否需要被释放.
fn main() {
// x 初始始化
let mut x = Box::new(42_i32);
// 创建可变更引用
let y = &mut x;
// x 被重新赋值, 旧的值自动被 drop
*y = Box::new(41);
// x 的作用域到此结束, drop 它
}
我们使用命令 rustc --emit asm static-drop.rs
生成对应的汇编代码,
下面展示了核心部分的代码, 并加上了几行注释:
.section .text._ZN11static_drop4main17h68890bb49a778ebaE,"ax",@progbits
.p2align 4, 0x90
.type _ZN11static_drop4main17h68890bb49a778ebaE,@function
_ZN11static_drop4main17h68890bb49a778ebaE:
.Lfunc_begin2:
.cfi_startproc
.cfi_personality 155, DW.ref.rust_eh_personality
.cfi_lsda 27, .Lexception2
subq $104, %rsp
.cfi_def_cfa_offset 112
.Ltmp6:
; malloc(4)
movl $4, %esi
movq %rsi, %rdi
callq _ZN5alloc5alloc15exchange_malloc17h73c35ae157034338E
.Ltmp7:
movq %rax, 40(%rsp)
jmp .LBB18_2
.LBB18_1:
.Ltmp8:
movq %rax, %rcx
movl %edx, %eax
movq %rcx, 88(%rsp)
movl %eax, 96(%rsp)
movq 88(%rsp), %rax
movq %rax, 32(%rsp)
jmp .LBB18_13
.LBB18_2:
; x.ptr = malloc(4)
; *(x.ptr) = 42
movq 40(%rsp), %rax
movl $42, (%rax)
movq %rax, 48(%rsp)
.Ltmp9:
; malloc(4)
movl $4, %esi
movq %rsi, %rdi
callq _ZN5alloc5alloc15exchange_malloc17h73c35ae157034338E
.Ltmp10:
; (x2.ptr) = malloc(4)
movq %rax, 24(%rsp)
jmp .LBB18_4
.LBB18_3:
.Ltmp11:
movq %rax, %rcx
movl %edx, %eax
movq %rcx, 72(%rsp)
movl %eax, 80(%rsp)
movq 72(%rsp), %rax
movq %rax, 8(%rsp)
movl 80(%rsp), %eax
movl %eax, 20(%rsp)
jmp .LBB18_6
.LBB18_4:
movq 24(%rsp), %rax
; *(x2.ptr) = 41
movl $41, (%rax)
jmp .LBB18_7
.LBB18_5:
.Ltmp15:
leaq 48(%rsp), %rdi
callq _ZN4core3ptr49drop_in_place$LT$alloc..boxed..Box$LT$i32$GT$$GT$17hac96f08cecbb6861E
.Ltmp16:
jmp .LBB18_12
.LBB18_6:
movl 20(%rsp), %eax
movq 8(%rsp), %rcx
movq %rcx, 56(%rsp)
movl %eax, 64(%rsp)
jmp .LBB18_5
.LBB18_7:
.Ltmp12:
; drop(x)
leaq 48(%rsp), %rdi
callq _ZN4core3ptr49drop_in_place$LT$alloc..boxed..Box$LT$i32$GT$$GT$17hac96f08cecbb6861E
.Ltmp13:
jmp .LBB18_10
.LBB18_8:
movq 24(%rsp), %rax
movq %rax, 48(%rsp)
jmp .LBB18_5
.LBB18_9:
.Ltmp14:
movq %rax, %rcx
movl %edx, %eax
movq %rcx, 56(%rsp)
movl %eax, 64(%rsp)
jmp .LBB18_8
.LBB18_10:
; x = x2
movq 24(%rsp), %rax
movq %rax, 48(%rsp)
leaq 48(%rsp), %rdi
; drop(x)
callq _ZN4core3ptr49drop_in_place$LT$alloc..boxed..Box$LT$i32$GT$$GT$17hac96f08cecbb6861E
addq $104, %rsp
.cfi_def_cfa_offset 8
retq
阅读汇编代码时, 最好对比着 Rust 代码, 方便理解.
但是汇编代码有上百行, 我们把汇编代码转译成 C 代码, 大概如下:
#include <stdlib.h>
#include <stdint.h>
int main(void) {
// let mut x = Box::new(42);
int32_t* x = (int32_t*) malloc(sizeof(int32_t));
*x = 42;
// let y = &mut x;
int32_t** y = &x;
// *y = Box::new(41);
int32_t* x2 = (int32_t*)malloc(sizeof(int32_t));
*x2 = 41;
free(x);
x = x2;
free(x);
return 0;
}
这个过程就比较清晰了吧, 编译上面的 C 代码, 并且用 valgrind
或者 sanitizers
等工具检测,
可以发现它进行了两次堆内存分配, 两次内存回收, 没有发现内存泄露的问题.
动态释放 dynamic drop
表达式有比较复杂的分支或者分支条件在运行期间才能判定, 通过在栈内存上设置 Drop Flag
来完成.
程序运行期间, 修改 drop-flag 标记, 来确定是否要调用该对象的 Drop
trait.
先看一个示例程序:
use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
let now = SystemTime::now();
let timestamp = now.duration_since(UNIX_EPOCH).unwrap_or_default();
let x: Box::<i32>;
if timestamp.as_millis() % 2 == 0 {
x = Box::new(42);
println!("x: {x}");
}
}
可以看到, 只有在程序运行时, 才能根据当前的时间标签决定要不要初始化变量 x, 这种情况就要用到 Drop Flag
了.
上面的 Rust 代码生成的汇编代码如下, 我们加入了一些注释:
.section .text._ZN12dynamic_drop4main17h353a883be865ee26E,"ax",@progbits
.p2align 4, 0x90
.type _ZN12dynamic_drop4main17h353a883be865ee26E,@function
_ZN12dynamic_drop4main17h353a883be865ee26E:
.Lfunc_begin3:
.cfi_startproc
.cfi_personality 155, DW.ref.rust_eh_personality
.cfi_lsda 27, .Lexception3
subq $248, %rsp
.cfi_def_cfa_offset 256
; 设置 x.drop-flag = 0
movb $0, 199(%rsp)
; let now = SystemTime::now();
movq _ZN3std4time10SystemTime3now17h4779e0425deae935E@GOTPCREL(%rip), %rax
callq *%rax
// now.seconds =
movq %rax, 48(%rsp)
// now.nano-seconds =
movl %edx, 56(%rsp)
; let timestamp = now.duration_since(UNIX_EPOCH).unwrap_or_default()
movq _ZN3std4time10SystemTime14duration_since17h0f40caf46c5e1553E@GOTPCREL(%rip), %rax
xorl %ecx, %ecx
movl %ecx, %edx
leaq 80(%rsp), %rdi
movq %rdi, 32(%rsp)
leaq 48(%rsp), %rsi
callq *%rax
movq 32(%rsp), %rdi
callq _ZN4core6result19Result$LT$T$C$E$GT$17unwrap_or_default17h8fe62a20db70e668E
; timestamp has value
// timestamp.seconds =
movq %rax, 64(%rsp)
// timestamp.nano-seconds =
movl %edx, 72(%rsp)
.Ltmp9:
; timestamp.as_millis()
leaq 64(%rsp), %rdi
callq _ZN4core4time8Duration9as_millis17h3157e191997c534eE
.Ltmp10:
movq %rax, 40(%rsp)
jmp .LBB23_4
.LBB23_1:
testb $1, 199(%rsp)
jne .LBB23_17
jmp .LBB23_16
.LBB23_2:
.Ltmp18:
movq %rax, %rcx
movl %edx, %eax
movq %rcx, 16(%rsp)
movl %eax, 28(%rsp)
jmp .LBB23_3
.LBB23_3:
movq 16(%rsp), %rcx
movl 28(%rsp), %eax
movq %rcx, 200(%rsp)
movl %eax, 208(%rsp)
jmp .LBB23_1
.LBB23_4:
jmp .LBB23_5
.LBB23_5:
; 判定 millis % 2 是否为 0
movq 40(%rsp), %rax
; test-bit(millis) == 1
testb $1, %al
jne .LBB23_9
jmp .LBB23_6
.LBB23_6:
; millis % 2 == 0 进入这个代码块
.Ltmp11:
; x = Box::new(42);
; malloc(4);
movl $4, %esi
movq %rsi, %rdi
callq _ZN5alloc5alloc15exchange_malloc17hbc6d664071ad5e2fE
.Ltmp12:
; x.ptr = xxx
movq %rax, 8(%rsp)
jmp .LBB23_8
.LBB23_7:
.Ltmp13:
movq %rax, %rcx
movl %edx, %eax
movq %rcx, 232(%rsp)
movl %eax, 240(%rsp)
movq 232(%rsp), %rcx
movl 240(%rsp), %eax
movq %rcx, 16(%rsp)
movl %eax, 28(%rsp)
jmp .LBB23_3
.LBB23_8:
movq 8(%rsp), %rax
; 设置堆内存上的值
; *(x.ptr) = 42;
movl $42, (%rax)
jmp .LBB23_10
.LBB23_9:
; millis % 2 == 1, 才进入这个分支
; 判断 x.drop_flag == 1
; 如果是 1, 就说明它初始化了, 需要被 drop
; 如果是 0, 就说明 x 是 uninit, 什么都不用做
testb $1, 199(%rsp)
jne .LBB23_15
jmp .LBB23_14
.LBB23_10:
movq 8(%rsp), %rax
; x.drop-flag = 1
movb $1, 199(%rsp)
; println!("x: {x}");
movq %rax, 104(%rsp)
leaq 104(%rsp), %rax
movq %rax, 216(%rsp)
leaq _ZN69_$LT$alloc..boxed..Box$LT$T$C$A$GT$$u20$as$u20$core..fmt..Display$GT$3fmt17h5ad2dd804fe02f48E(%rip), %rax
movq %rax, 224(%rsp)
movq 216(%rsp), %rax
movq %rax, 176(%rsp)
movq 224(%rsp), %rax
movq %rax, 184(%rsp)
movups 176(%rsp), %xmm0
movaps %xmm0, 160(%rsp)
.Ltmp14:
leaq .L__unnamed_9(%rip), %rsi
leaq 112(%rsp), %rdi
movl $2, %edx
leaq 160(%rsp), %rcx
movl $1, %r8d
callq _ZN4core3fmt9Arguments6new_v117hd2ff9f250d646380E
.Ltmp15:
jmp .LBB23_12
.LBB23_12:
.Ltmp16:
movq _ZN3std2io5stdio6_print17h8f9e07feda690a3dE@GOTPCREL(%rip), %rax
leaq 112(%rsp), %rdi
callq *%rax
.Ltmp17:
jmp .LBB23_13
.LBB23_13:
; if millis % 2 == 0 { ... } 代码块运行完成
; 进入最后的清理阶段
jmp .LBB23_9
.LBB23_14:
; return 0
movb $0, 199(%rsp)
addq $248, %rsp
.cfi_def_cfa_offset 8
retq
.LBB23_15:
.cfi_def_cfa_offset 256
; 这个是正常的工作流调用的
; drop(x);
leaq 104(%rsp), %rdi
callq _ZN4core3ptr49drop_in_place$LT$alloc..boxed..Box$LT$i32$GT$$GT$17ha5010c067d13d768E
jmp .LBB23_14
.LBB23_16:
movq 200(%rsp), %rdi
callq _Unwind_Resume@PLT
.LBB23_17:
.Ltmp19:
; 这个是处理 unwind 异常时调用的
; drop(x);
leaq 104(%rsp), %rdi
callq _ZN4core3ptr49drop_in_place$LT$alloc..boxed..Box$LT$i32$GT$$GT$17ha5010c067d13d768E
.Ltmp20:
jmp .LBB23_16
.LBB23_18:
.Ltmp21:
movq _ZN4core9panicking16panic_in_cleanup17hd62aa59d1fda1c9fE@GOTPCREL(%rip), %rax
callq *%rax
.Lfunc_end23:
.size _ZN12dynamic_drop4main17h353a883be865ee26E, .Lfunc_end23-_ZN12dynamic_drop4main17h353a883be865ee26E
.cfi_endproc
其行为如下:
- 栈空间初始化完成后, 就设置变量 x 的
drop-flag = 0
- 然后计算当前的时间标签, 判断是否为偶数
- 如果为偶数, 继续
- 如果为奇数, 跳转到第4步
- 分配堆内存, 并设置内存里的值为
42
; 初始化 x, 并设置x.drop-flag = 1
- 组装参数, 调用
print()
打印字符串
- 组装参数, 调用
- 判断
x.drop-flag == 1
, 如果是1
, 就调用Box::drop(&mut x)
来释放它
我们将汇编代码的行为, 作为注释加入到原先的 Rust 代码中, 更方便阅读:
use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
// 设置 x 的 Drop Flag
// x.drop-flag = 0;
let now = SystemTime::now();
let timestamp = now.duration_since(UNIX_EPOCH).unwrap_or_default();
let x: Box::<i32>;
if timestamp.as_millis() % 2 == 0 {
// 设置 x.drop-flag = 1
// 为 x 分配堆内存, 并设置其值为 42
x = Box::new(42);
println!("x: {x}");
// 设置 x.drop-flag = 0
// 调用 core::mem::drop(x);
drop(x);
}
// 判断 x.drop-flag
// if x.drop-flag == 1 {
// core::ptr::drop_in_place(*x as *mut i32);
// }
}
我们甚至可以将上面的汇编代码转译成对应的 C 代码:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <time.h>
int main(void) {
bool x_drop_flag = false;
int32_t* x;
struct timespec now;
if (clock_gettime(CLOCK_REALTIME, &now) == -1) {
// Ignored
}
int64_t millis = now.tv_sec * 1000 + now.tv_nsec / 1000000;
if (millis % 2 == 0) {
x = (int32_t*) malloc(sizeof(int32_t));
*x = 42;
x_drop_flag = true;
printf("x: %d\n", *x);
}
if (x_drop_flag) {
free(x);
}
return 0;
}
更有趣的是, 我们可以用 gdb/lldb 来手动修改 x.drop-flag
, 如果把它设置为 1
, 并且 x
未初始化的话,
在进程结束时, 就可能会产生段错误 segfault.
dynamic-drop`dynamic_drop::main::h5787b1b14685d565:
0x5555555696e0 <+0>: subq $0x118, %rsp ; imm = 0x118
0x5555555696e7 <+7>: movb $0x0, 0xcf(%rsp)
-> 0x5555555696ef <+15>: movq 0x4165a(%rip), %rax
0x5555555696f6 <+22>: callq *%rax
0x5555555696f8 <+24>: movq %rax, 0x30(%rsp)
上面展示的是 main() 函数初始化时的代码, 它调整完栈顶后, 立即重置了 x.drop-flag = 0
.
在后面的代码运行前, 我们可以使用命令 p *(char*)($rsp + 0xcf) = 1
将 x.drop-flag
设置为1
.
等进程结束时, x
超出了作用域, 就要检查 x.drop-flag
的值. 如果x
未初始化的话, 它内部的
指针可能指向任意的地址, 所以就产生了段错误.
我们再看一下段错误时的函数的调用栈:
* thread #1, name = 'dynamic-drop', stop reason = signal SIGSEGV: invalid address (fault address: 0xe8)
frame #0: 0x00007ffff7e0a6aa libc.so.6`__GI___libc_free(mem=0x00000000000000f0) at malloc.c:3375:7
(lldb) bt
* thread #1, name = 'dynamic-drop', stop reason = signal SIGSEGV: invalid address (fault address: 0xe8)
* frame #0: 0x00007ffff7e0a6aa libc.so.6`__GI___libc_free(mem=0x00000000000000f0) at malloc.c:3375:7
frame #1: 0x000055555556a000 dynamic-drop`_$LT$alloc..alloc..Global$u20$as$u20$core..alloc..Allocator$GT$::deallocate::hfe4b1fe0680a312e at alloc.rs:119:14
frame #2: 0x0000555555569fcd dynamic-drop`_$LT$alloc..alloc..Global$u20$as$u20$core..alloc..Allocator$GT$::deallocate::hfe4b1fe0680a312e(self=0x00007fffffff
dd00, ptr=(pointer = ""), layout=Layout @ 0x00007fffffffdb88) at alloc.rs:256:22
frame #3: 0x0000555555569b89 dynamic-drop`_$LT$alloc..boxed..Box$LT$T$C$A$GT$$u20$as$u20$core..ops..drop..Drop$GT$::drop::hea3c2fa5449fa588(self=0x00007ffff
fffdcf8) at boxed.rs:1247:17
frame #4: 0x0000555555569ae8 dynamic-drop`core::ptr::drop_in_place$LT$alloc..boxed..Box$LT$i32$GT$$GT$::h4bec233740204caa((null)=0x00007fffffffdcf8) at mod.
rs:514:1
手动调用 drop()
函数
上面的代码演示了 Drop Flag
是如何工作的, 接下来, 我们看一下手动调用 drop()
函数释放了对象后,
它的行为是怎么样的?
先看示例代码:
use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
let now = SystemTime::now();
let timestamp = now.duration_since(UNIX_EPOCH).unwrap_or_default();
let x: Box::<i32>;
if timestamp.as_millis() % 2 == 0 {
x = Box::new(42);
println!("x: {x}");
drop(x);
}
}
将上面的代码生成汇编代码, 我们还加上了几条注释:
.section .text._ZN11manual_drop4main17h6a90a7c6667c6acfE,"ax",@progbits
.p2align 4, 0x90
.type _ZN11manual_drop4main17h6a90a7c6667c6acfE,@function
_ZN11manual_drop4main17h6a90a7c6667c6acfE:
.Lfunc_begin3:
.cfi_startproc
.cfi_personality 155, DW.ref.rust_eh_personality
.cfi_lsda 27, .Lexception3
subq $248, %rsp
.cfi_def_cfa_offset 256
movb $0, 199(%rsp)
movq _ZN3std4time10SystemTime3now17h4779e0425deae935E@GOTPCREL(%rip), %rax
callq *%rax
movq %rax, 48(%rsp)
movl %edx, 56(%rsp)
movq _ZN3std4time10SystemTime14duration_since17h0f40caf46c5e1553E@GOTPCREL(%rip), %rax
xorl %ecx, %ecx
movl %ecx, %edx
leaq 80(%rsp), %rdi
movq %rdi, 32(%rsp)
leaq 48(%rsp), %rsi
callq *%rax
movq 32(%rsp), %rdi
callq _ZN4core6result19Result$LT$T$C$E$GT$17unwrap_or_default17h28c150cee05a8583E
movq %rax, 64(%rsp)
movl %edx, 72(%rsp)
.Ltmp9:
leaq 64(%rsp), %rdi
callq _ZN4core4time8Duration9as_millis17hd86e02e1e172ae4fE
.Ltmp10:
movq %rax, 40(%rsp)
jmp .LBB24_4
.LBB24_1:
; 检查 x.drop-flag == 1
testb $1, 199(%rsp)
jne .LBB24_16
jmp .LBB24_15
.LBB24_2:
.Ltmp20:
movq %rax, %rcx
movl %edx, %eax
movq %rcx, 16(%rsp)
movl %eax, 28(%rsp)
jmp .LBB24_3
.LBB24_3:
movq 16(%rsp), %rcx
movl 28(%rsp), %eax
movq %rcx, 200(%rsp)
movl %eax, 208(%rsp)
jmp .LBB24_1
.LBB24_4:
jmp .LBB24_5
.LBB24_5:
movq 40(%rsp), %rax
testb $1, %al
jne .LBB24_9
jmp .LBB24_6
.LBB24_6:
.Ltmp11:
; 进入 millis % 2 == 1 的分支
; malloc(4)
movl $4, %esi
movq %rsi, %rdi
callq _ZN5alloc5alloc15exchange_malloc17h48568ba0c1cf90faE
.Ltmp12:
movq %rax, 8(%rsp)
jmp .LBB24_8
.LBB24_7:
.Ltmp13:
movq %rax, %rcx
movl %edx, %eax
movq %rcx, 232(%rsp)
movl %eax, 240(%rsp)
movq 232(%rsp), %rcx
movl 240(%rsp), %eax
movq %rcx, 16(%rsp)
movl %eax, 28(%rsp)
jmp .LBB24_3
.LBB24_8:
; x.ptr = malloc(4);
movq 8(%rsp), %rax
; *(x.ptr) = 42
movl $42, (%rax)
jmp .LBB24_10
.LBB24_9:
movb $0, 199(%rsp)
addq $248, %rsp
.cfi_def_cfa_offset 8
retq
.LBB24_10:
.cfi_def_cfa_offset 256
movq 8(%rsp), %rax
; x.drop-flag = 1
movb $1, 199(%rsp)
movq %rax, 104(%rsp)
leaq 104(%rsp), %rax
movq %rax, 216(%rsp)
leaq _ZN69_$LT$alloc..boxed..Box$LT$T$C$A$GT$$u20$as$u20$core..fmt..Display$GT$3fmt17h2b03e6eb572a9ffaE(%rip), %rax
movq %rax, 224(%rsp)
movq 216(%rsp), %rax
movq %rax, 176(%rsp)
movq 224(%rsp), %rax
movq %rax, 184(%rsp)
movups 176(%rsp), %xmm0
movaps %xmm0, 160(%rsp)
.Ltmp14:
leaq .L__unnamed_9(%rip), %rsi
leaq 112(%rsp), %rdi
movl $2, %edx
leaq 160(%rsp), %rcx
movl $1, %r8d
callq _ZN4core3fmt9Arguments6new_v117h86651149b4254342E
.Ltmp15:
jmp .LBB24_12
.LBB24_12:
.Ltmp16:
movq _ZN3std2io5stdio6_print17h8f9e07feda690a3dE@GOTPCREL(%rip), %rax
leaq 112(%rsp), %rdi
callq *%rax
.Ltmp17:
jmp .LBB24_13
.LBB24_13:
; x.drop-flag = 0
movb $0, 199(%rsp)
; drop(x)
movq 104(%rsp), %rdi
.Ltmp18:
callq _ZN4core3mem4drop17hf19ef99eb1293173E
.Ltmp19:
jmp .LBB24_14
.LBB24_14:
jmp .LBB24_9
.LBB24_15:
movq 200(%rsp), %rdi
callq _Unwind_Resume@PLT
.LBB24_16:
.Ltmp21:
leaq 104(%rsp), %rdi
callq _ZN4core3ptr49drop_in_place$LT$alloc..boxed..Box$LT$i32$GT$$GT$17h668a38bfbe5d4573E
.Ltmp22:
jmp .LBB24_15
.LBB24_17:
.Ltmp23:
movq _ZN4core9panicking16panic_in_cleanup17hd62aa59d1fda1c9fE@GOTPCREL(%rip), %rax
callq *%rax
.Lfunc_end24:
.size _ZN11manual_drop4main17h6a90a7c6667c6acfE, .Lfunc_end24-_ZN11manual_drop4main17h6a90a7c6667c6acfE
.cfi_endproc
可以看到, 当执行到 drop(x);
时, 编译器:
- 先重置
x.drop-flag = 0
- 接着调用
core::mem::drop(x);
而编译器自动释放对象 x
时, 会调用另一个函数
core::ptr::drop_in_place(*x as *mut i32)
.
将上面的汇编代码合并到之前的 Rust 代码, 大致如下:
use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
// 设置 x 的 Drop Flag
// x.drop-flag = 0;
let now = SystemTime::now();
let timestamp = now.duration_since(UNIX_EPOCH).unwrap_or_default();
let x: Box::<i32>;
if timestamp.as_millis() % 2 == 0 {
// 设置 x.drop-flag = 1
// 为 x 分配堆内存, 并设置其值为 42
x = Box::new(42);
println!("x: {x}");
// 设置 x.drop-flag = 0
// 调用 core::mem::drop(x);
drop(x);
}
// 判断 x.drop-flag
// if x.drop-flag == 1 {
// core::ptr::drop_in_place(*x as *mut i32);
// }
}
Drop 是零成本抽像吗?
我们分析了上面的 Rust 程序, 可以明显地发现, 编译器生成的代码在支持动态 drop 时, 需要反复地判断
drop-flag 是不是被设置, 如果被设置成1, 就要调用该类型的 Drop
trait.
这种行为, 跟我们在 C 代码中手动判断指针是否为 NULL 是一样的, 每次给变量分配新的堆内存之前, 就要先判定一下它的当前是否为空指针:
int* x;
if (x != NULL) {
free(x);
}
x = malloc(4);
...
if (x != NULL) {
free(x);
}
x = malloc(4);
...
但这些条件判断代码, Rust 编译器自动帮我们生成了, 而且可以保证没有泄露.
不要自动 Drop
到这里, 就要进入内存管理的深水区了, 上面提到了 Rust 会帮我们自动管理内存, 在合适的时机自动调用
对象的 Drop
trait.
但与此同是, Rust 标准库中提供了一些手段, 可以让我们绕过这个机制, 但好在它们大都是 unsafe
的.
遇到这些代码, 要打起精神, 因为 Rustc 编译器可能帮不上你了.
ManuallyDrop
ManuallyDrop 做了什么? 对于栈上的对象, 不需要调用该对象的 Drop
trait.
先看一个 ManuallyDrop 的一个例子:
use std::mem::ManuallyDrop;
use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
let now = SystemTime::now();
let timestamp = now.duration_since(UNIX_EPOCH).unwrap_or_default();
let x: Box::<i32>;
let millis = timestamp.as_millis();
if millis % 2 == 0 {
x = Box::new(42);
println!("x: {x}");
let _x_no_dropping = ManuallyDrop::new(x);
} else if millis % 3 == 0 {
x = Box::new(41);
println!("x: {x}");
}
}
上面的代码, 如果 millis
是偶数的话, x 会被标记为 ManuallyDrop
, 这样的话编译器将不再自动
调用它的 Drop
trait, 这里就是一个内存泄露点.
我们来看一下生成的汇编代码:
.section .text._ZN13manually_drop4main17hc0c2c79e8eb75025E,"ax",@progbits
.p2align 4, 0x90
.type _ZN13manually_drop4main17hc0c2c79e8eb75025E,@function
_ZN13manually_drop4main17hc0c2c79e8eb75025E:
.Lfunc_begin3:
.cfi_startproc
.cfi_personality 155, DW.ref.rust_eh_personality
.cfi_lsda 27, .Lexception3
subq $408, %rsp
.cfi_def_cfa_offset 416
; x.drop-flag = 0
movb $0, 319(%rsp)
movq _ZN3std4time10SystemTime3now17h4779e0425deae935E@GOTPCREL(%rip), %rax
callq *%rax
movq %rax, 80(%rsp)
movl %edx, 88(%rsp)
movq _ZN3std4time10SystemTime14duration_since17h0f40caf46c5e1553E@GOTPCREL(%rip), %rax
xorl %ecx, %ecx
movl %ecx, %edx
leaq 112(%rsp), %rdi
movq %rdi, 56(%rsp)
leaq 80(%rsp), %rsi
callq *%rax
movq 56(%rsp), %rdi
callq _ZN4core6result19Result$LT$T$C$E$GT$17unwrap_or_default17hb4028d84d22833d3E
movq %rax, 96(%rsp)
movl %edx, 104(%rsp)
.Ltmp9:
leaq 96(%rsp), %rdi
callq _ZN4core4time8Duration9as_millis17h1c5ed4310d34772cE
.Ltmp10:
movq %rdx, 64(%rsp)
movq %rax, 72(%rsp)
jmp .LBB23_5
.LBB23_1:
; x.drop-flag == 1
testb $1, 319(%rsp)
jne .LBB23_28
jmp .LBB23_27
.LBB23_2:
.Ltmp25:
movq %rax, %rcx
movl %edx, %eax
movq %rcx, 40(%rsp)
movl %eax, 52(%rsp)
jmp .LBB23_3
.LBB23_3:
movq 40(%rsp), %rcx
movl 52(%rsp), %eax
movq %rcx, 24(%rsp)
movl %eax, 36(%rsp)
jmp .LBB23_4
.LBB23_4:
movq 24(%rsp), %rcx
movl 36(%rsp), %eax
movq %rcx, 320(%rsp)
movl %eax, 328(%rsp)
jmp .LBB23_1
.LBB23_5:
jmp .LBB23_6
.LBB23_6:
; millis % 2 == 0
movq 72(%rsp), %rax
testb $1, %al
jne .LBB23_10
jmp .LBB23_7
.LBB23_7:
.Ltmp18:
movl $4, %esi
movq %rsi, %rdi
callq _ZN5alloc5alloc15exchange_malloc17he729bf437884de0dE
.Ltmp19:
movq %rax, 16(%rsp)
jmp .LBB23_9
.LBB23_8:
.Ltmp20:
movq %rax, %rcx
movl %edx, %eax
movq %rcx, 392(%rsp)
movl %eax, 400(%rsp)
movq 392(%rsp), %rcx
movl 400(%rsp), %eax
movq %rcx, 40(%rsp)
movl %eax, 52(%rsp)
jmp .LBB23_3
.LBB23_9:
movq 16(%rsp), %rax
; *(x.ptr) = 42
movl $42, (%rax)
jmp .LBB23_11
.LBB23_10:
jmp .LBB23_17
.LBB23_11:
movq 16(%rsp), %rax
; x.drop-flag = 1
movb $1, 319(%rsp)
movq %rax, 136(%rsp)
leaq 136(%rsp), %rax
movq %rax, 352(%rsp)
leaq _ZN69_$LT$alloc..boxed..Box$LT$T$C$A$GT$$u20$as$u20$core..fmt..Display$GT$3fmt17h2d3ff932e53a7b07E(%rip), %rax
movq %rax, 360(%rsp)
movq 352(%rsp), %rax
movq %rax, 208(%rsp)
movq 360(%rsp), %rax
movq %rax, 216(%rsp)
movups 208(%rsp), %xmm0
movaps %xmm0, 192(%rsp)
.Ltmp21:
leaq .L__unnamed_9(%rip), %rsi
leaq 144(%rsp), %rdi
movl $2, %edx
leaq 192(%rsp), %rcx
movl $1, %r8d
callq _ZN4core3fmt9Arguments6new_v117hd27b08a38d223f7cE
.Ltmp22:
jmp .LBB23_13
.LBB23_13:
.Ltmp23:
movq _ZN3std2io5stdio6_print17h8f9e07feda690a3dE@GOTPCREL(%rip), %rax
leaq 144(%rsp), %rdi
callq *%rax
.Ltmp24:
jmp .LBB23_14
.LBB23_14:
; x.drop-flag = 0
; let _x_no_dropping = ManuallyDrop::new(x)
movb $0, 319(%rsp)
movq 136(%rsp), %rax
movq %rax, 368(%rsp)
jmp .LBB23_16
.LBB23_16:
testb $1, 319(%rsp)
jne .LBB23_26
jmp .LBB23_25
.LBB23_17:
movq 72(%rsp), %rax
movabsq $-6148914691236517206, %rcx
movq %rax, %rdi
imulq %rcx, %rdi
movabsq $-6148914691236517205, %rcx
movq %rcx, 8(%rsp)
mulq %rcx
movq %rax, %rsi
movq 64(%rsp), %rax
movq %rdx, %rcx
movq 8(%rsp), %rdx
addq %rdi, %rcx
imulq %rdx, %rax
addq %rax, %rcx
movabsq $6148914691236517205, %rax
movq %rax, %rdx
subq %rsi, %rdx
sbbq %rcx, %rax
jb .LBB23_16
jmp .LBB23_18
.LBB23_18:
.Ltmp11:
movl $4, %esi
movq %rsi, %rdi
callq _ZN5alloc5alloc15exchange_malloc17he729bf437884de0dE
.Ltmp12:
movq %rax, (%rsp)
jmp .LBB23_20
.LBB23_19:
.Ltmp13:
movq %rax, %rcx
movl %edx, %eax
movq %rcx, 376(%rsp)
movl %eax, 384(%rsp)
movq 376(%rsp), %rcx
movl 384(%rsp), %eax
movq %rcx, 24(%rsp)
movl %eax, 36(%rsp)
jmp .LBB23_4
.LBB23_20:
movq (%rsp), %rax
; *(x.ptr) = 41;
movl $41, (%rax)
movq (%rsp), %rax
; x.drop-flag = 1
movb $1, 319(%rsp)
movq %rax, 136(%rsp)
leaq 136(%rsp), %rax
movq %rax, 336(%rsp)
leaq _ZN69_$LT$alloc..boxed..Box$LT$T$C$A$GT$$u20$as$u20$core..fmt..Display$GT$3fmt17h2d3ff932e53a7b07E(%rip), %rax
movq %rax, 344(%rsp)
movq 336(%rsp), %rax
movq %rax, 296(%rsp)
movq 344(%rsp), %rax
movq %rax, 304(%rsp)
movups 296(%rsp), %xmm0
movaps %xmm0, 272(%rsp)
.Ltmp14:
leaq .L__unnamed_9(%rip), %rsi
leaq 224(%rsp), %rdi
movl $2, %edx
leaq 272(%rsp), %rcx
movl $1, %r8d
callq _ZN4core3fmt9Arguments6new_v117hd27b08a38d223f7cE
.Ltmp15:
jmp .LBB23_23
.LBB23_23:
.Ltmp16:
movq _ZN3std2io5stdio6_print17h8f9e07feda690a3dE@GOTPCREL(%rip), %rax
leaq 224(%rsp), %rdi
callq *%rax
.Ltmp17:
jmp .LBB23_24
.LBB23_24:
jmp .LBB23_16
.LBB23_25:
movb $0, 319(%rsp)
addq $408, %rsp
.cfi_def_cfa_offset 8
retq
.LBB23_26:
.cfi_def_cfa_offset 416
; core::ptr::drop_in_place(x)
leaq 136(%rsp), %rdi
callq _ZN4core3ptr49drop_in_place$LT$alloc..boxed..Box$LT$i32$GT$$GT$17h52d911587572c48aE
jmp .LBB23_25
.LBB23_27:
movq 320(%rsp), %rdi
callq _Unwind_Resume@PLT
.LBB23_28:
.Ltmp26:
; drop(x);
leaq 136(%rsp), %rdi
callq _ZN4core3ptr49drop_in_place$LT$alloc..boxed..Box$LT$i32$GT$$GT$17h52d911587572c48aE
.Ltmp27:
jmp .LBB23_27
.LBB23_29:
.Ltmp28:
movq _ZN4core9panicking16panic_in_cleanup17hd62aa59d1fda1c9fE@GOTPCREL(%rip), %rax
callq *%rax
.Lfunc_end23:
.size _ZN13manually_drop4main17hc0c2c79e8eb75025E, .Lfunc_end23-_ZN13manually_drop4main17hc0c2c79e8eb75025E
.cfi_endproc
上面的汇编代码比较长, 将它的行为作为注释加到原先的 Rust 代码中, 更容易阅读:
use std::mem::ManuallyDrop;
use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
// 重置 x 的 Drop Flag:
// x.drop-flag = 0
let now = SystemTime::now();
let timestamp = now.duration_since(UNIX_EPOCH).unwrap_or_default();
let x: Box::<i32>;
let millis = timestamp.as_millis();
if millis % 2 == 0 {
// 设置 x 的 Drop Flag:
// x.drop-flag = 1
// 为 x 分配堆内存, 并设置它的值为42
x = Box::new(42);
println!("x: {x}");
// 这里, ManuallyDrop 会重置 x 的 Drop Flag:
// x.drop-flag = 0
let _x_no_dropping = ManuallyDrop::new(x);
} else if millis % 3 == 0 {
// 设置 x 的 Drop Flag:
// x.drop-flag = 1
// 为 x 分配堆内存, 并设置它的值为41
x = Box::new(41);
println!("x: {x}");
}
// x 的值超出作用域, 判断要不要 drop 它:
// if x.drop-flag == 1 {
// core::ptr::drop_in_place(x);
// }
}
Box::leak
另一个例子是 Box::leak()
它也会抑制编译器自动调用对象的 Drop
trait.
看下面的例子, 也会产生内存泄露:
use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
let now = SystemTime::now();
let timestamp = now.duration_since(UNIX_EPOCH).unwrap_or_default();
let x: Box::<i32>;
let millis = timestamp.as_millis();
if millis % 2 == 0 {
x = Box::new(42);
println!("x: {x}");
let _x_ptr = Box::leak(x);
} else if millis % 3 == 0 {
x = Box::new(41);
println!("x: {x}");
}
}
我们追踪 Box::leak()
的源代码可以发现, 它的内部也是调用了 ManuallyDrop::new()
的:
impl Box {
#[inline]
pub fn leak<'a>(b: Self) -> &'a mut T
where
A: 'a,
{
unsafe { &mut *Box::into_raw(b) }
}
#[inline]
pub fn into_raw(b: Self) -> *mut T {
// Make sure Miri realizes that we transition from a noalias pointer to a raw pointer here.
unsafe { addr_of_mut!(*&mut *Self::into_raw_with_allocator(b).0) }
}
pub fn into_raw_with_allocator(b: Self) -> (*mut T, A) {
let mut b = mem::ManuallyDrop::new(b);
// We carefully get the raw pointer out in a way that Miri's aliasing model understands what
// is happening: using the primitive "deref" of `Box`. In case `A` is *not* `Global`, we
// want *no* aliasing requirements here!
// In case `A` *is* `Global`, this does not quite have the right behavior; `into_raw`
// works around that.
let ptr = addr_of_mut!(**b);
let alloc = unsafe { ptr::read(&b.1) };
(ptr, alloc)
}
}
ptr 模块
最后一个要介绍的是 ptr
模块中的几个函数:
- write()
- copy()
- copy_nonoverlapping()
它们也会抑制编译器自动调用对象的 Drop
trait.
我们不再举例了, 而是直接看一下 Vec<T>
的源代码, 看它是怎么实现插入元素和弹出元素的;
use std::ptr;
impl<T> Vec<T> {
#[inline]
pub fn push(&mut self, value: T) {
// Inform codegen that the length does not change across grow_one().
let len = self.len;
// This will panic or abort if we would allocate > isize::MAX bytes
// or if the length increment would overflow for zero-sized types.
if len == self.buf.capacity() {
self.buf.grow_one();
}
unsafe {
let end = self.as_mut_ptr().add(len);
ptr::write(end, value);
self.len = len + 1;
}
}
#[inline]
pub fn pop(&mut self) -> Option<T> {
if self.len == 0 {
None
} else {
unsafe {
self.len -= 1;
core::hint::assert_unchecked(self.len < self.capacity());
Some(ptr::read(self.as_ptr().add(self.len())))
}
}
}
}
版权
本文节选自 Rust 编程入门 Introduction to Rust 在线电子书.