Rust 中调用 Drop 的时机

42 阅读4分钟

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 等工具检测, 可以发现它进行了两次堆内存分配, 两次内存回收, 没有发现内存泄露的问题.

static-drop.svg

动态释放 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

其行为如下:

  1. 栈空间初始化完成后, 就设置变量 x 的 drop-flag = 0
  2. 然后计算当前的时间标签, 判断是否为偶数
    • 如果为偶数, 继续
    • 如果为奇数, 跳转到第4步
  3. 分配堆内存, 并设置内存里的值为 42; 初始化 x, 并设置 x.drop-flag = 1
    • 组装参数, 调用 print() 打印字符串
  4. 判断 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) = 1x.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 在线电子书.

参考