您现在的位置是:首页 > 文章详情

Rust 中调用 Drop 的时机

日期:2025-01-07点击:73

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 

其行为如下:

  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>在线电子书, 转载请注明出处.

参考

原文链接:https://my.oschina.net/shaohua2024/blog/15680818
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章