Rust 中调用 Drop 的时机
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>在线电子书, 转载请注明出处.
参考

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
腾讯回应被列入美国防部名单:是个错误
1月7日早间,针对美国国防部将腾讯公司列入中国军工公司名单(根据美国法律正式规定为“第1260H条清单”),腾讯回应称, “腾讯被列入这份名单显然是一个错误,我们并不是军工企业或军工供应商。不同于出口管制或其他,这份清单对我们的业务没有影响。尽管如此,我们仍将同美国相关部门共同解决误会。” 美国国防部在当地时间周一表示,已将包括腾讯、宁德时代在内的中国科技巨头列入名单。 据悉,美国2021财年《国防授权法案》第1260H条,美国国防部每年更新并公布一份与中国军方有联系的实体清单。
- 下一篇
2025:智能世界的 50 个潜在可能
2025 年的总体特征,可以用 “确定性海啸、多领域临界点、AI 自身范式转变、万物智能体、极化有反身性、亟需群体智能” 这 6 点来概括。 今天发表我经过持续预研所做出的新年预判:“2025 年的 50 个可能”,分别从生态、业态、技术、产品、企业等五个角度展开。均为对可能发生之事的预判,不是对已发生之事的罗列。仅供参考。 非常确定 2025 年大模型应用进入大幅增长期,更重要的是:“确定性” 本身进入临界点。2024 年基本面的一个重要变化是,主要各方完成了对 AI2.0 的确认,对 AI 原理演进、技术能力、产业价值、发展路径、应用加速、商业闭环的确认 - 确信 - 确定。 确定不是泡沫豪赌,确定必然捧得 AGI 圣杯,确定算力和能源其实可持续,确定千行百业数十亿人必然广泛使用,确定 AI2.0 会从文本数据走向现实世界,确定产业飞轮可以闭环,确定这是一个亿万价值的万亿产业…… 本来就已经冲击力十足的 AI2.0 风暴,经过确认之后,各方更是加速 All In,加之应用已到可用和高可用区间,市场加速成长。AI 2.0 的能量迸发因此必然而然。从此潮流滚滚。 如果说 30 年前在中...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- Mario游戏-低调大师作品
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题