Rust如何知道是否在堆栈展开期间运行析构函数?

时间:2016-09-28 14:45:45

标签: rust destructor undefined-behavior stack-unwinding

mem::uninitialized的文档指出了使用该函数危险/不安全的原因:在未初始化的内存上调用drop是未定义的行为。

因此,我相信这段代码应该是未定义的:

let a: TypeWithDrop = unsafe { mem::uninitialized() };
panic!("=== Testing ==="); // Destructor of `a` will be run (U.B)

但是,我编写了这段代码,它在安全的Rust中工作,并且似乎没有受到未定义的行为的影响:

#![feature(conservative_impl_trait)]

trait T {
    fn disp(&mut self);
}

struct A;
impl T for A {
    fn disp(&mut self) { println!("=== A ==="); }
}
impl Drop for A {
    fn drop(&mut self) { println!("Dropping A"); }
}

struct B;
impl T for B {
    fn disp(&mut self) { println!("=== B ==="); }
}
impl Drop for B {
    fn drop(&mut self) { println!("Dropping B"); }
}

fn foo() -> impl T { return A; }
fn bar() -> impl T { return B; }

fn main() {
    let mut a;
    let mut b;

    let i = 10;
    let t: &mut T = if i % 2 == 0 {
        a = foo();
        &mut a
    } else {
        b = bar();
        &mut b
    };

    t.disp();
    panic!("=== Test ===");
}

似乎总是执行正确的析构函数,而忽略另一个析构函数。如果我尝试使用ab(例如a.disp()而不是t.disp()),则会错误地说我可能正在使用未初始化的内存。让我感到惊讶的是panic王,无论i的价值是什么,它总是运行正确的析构函数(打印预期的字符串)。

这是怎么发生的?如果运行时可以确定要运行哪个析构函数,那么是否需要为实现Drop的类型初始化有关内存的部分是否应从上面链接的mem::uninitialized()文档中删除?

3 个答案:

答案 0 :(得分:21)

使用drop flags

Rust(包括版本1.12)在每个类型实现Drop的值中存储一个布尔标志(从而将该类型的大小增加一个字节)。该标志决定是否运行析构函数。因此,当您执行b = bar()时,它会为b变量设置标志,因此只运行b的析构函数。反之亦然a

请注意,从Rust版本1.13(在编写beta编译器时)开始,该标志不存储在类型中,而是存储在每个变量或临时的堆栈中。这是通过Rust编译器中MIR的出现实现的。 MIR显着简化了Rust代码到机器代码的转换,从而使此功能能够将drop标记移动到堆栈。优化通常会消除该标志,如果他们能够在编译时弄清楚哪个对象将被删除。

通过查看类型的大小,您可以在Rust编译器中“观察”此标志,最高版本为1.12:

struct A;

struct B;

impl Drop for B {
    fn drop(&mut self) {}
}

fn main() {
    println!("{}", std::mem::size_of::<A>());
    println!("{}", std::mem::size_of::<B>());
}

在堆栈标志之前分别打印01,在堆栈标志之前打印00

然而,使用mem::uninitialized仍然是不安全的,因为编译器仍然可以看到a变量的赋值并设置drop标志。因此,析构函数将在未初始化的内存上调用。请注意,在您的示例中,Drop impl不会访问您的类型的任何内存(drop flag除外,但对您来说是不可见的)。因此,您无法访问未初始化的内存(无论如何都是零字节,因为您的类型是零大小的结构)。据我所知,这意味着您的unsafe { std::mem::uninitialized() }代码实际上是安全的,因为之后不会出现内存不安全的情况。

答案 1 :(得分:17)

这里隐藏着两个问题:

  1. 编译器如何跟踪哪个变量被初始化?
  2. 为什么用mem::uninitialized()初始化会导致未定义的行为?
  3. 让我们按顺序解决它们。

      

    编译器如何跟踪哪个变量被初始化?

    编译器为范围结束时必须运行Drop的每个变量注入所谓的&#34; drop flags&#34;:在栈上注入一个布尔标志,说明是否变量需要处理。

    旗帜开始&#34;没有&#34;,移动到&#34;是&#34;如果变量被初始化,则回到&#34; no&#34;如果变量是从。

    移动的

    最后,当有时间删除此变量时,将检查该标志,并在必要时将其删除。

    这与编译器的流量分析是否会抱怨可能未初始化的变量无关:只有在满足流量分析时才生成代码。

      

    为什么用mem::uninitialized()进行初始化会导致未定义的行为?

    使用mem::uninitialized()时,您向编译器做出承诺:不要担心,我绝对要初始化

    就编译器而言,变量因此被完全初始化,并且drop标志被设置为&#34; yes&#34; (直到你离开它为止。)

    反过来,这意味着将调用Drop

    使用未初始化的对象是Undefined Behavior,而代表您在未初始化对象上调用Drop的编译器计为&#34;使用它&#34;。

    加成:

      

    在我的测试中,没有发生任何奇怪的事情!

    请注意,未定义的行为意味着任何事情都可能发生;不幸的是,任何事情,包括&#34;似乎工作&#34; (或者甚至&#34;尽管有可能而且按预期工作&#34;)。

    特别是,如果你不在Drop::drop(只是打印)中访问对象的内存,那么很可能一切都会正常工作。但是,如果你确实访问它,你可能会看到奇怪的整数,指向野外的指针等......

    如果优化器很聪明,即使没有访问它,也可能会做奇怪的事情!由于我们使用的是LLVM,我邀请您阅读Chris Lattner(LLVM的父亲)的What every C programmer should know about Undefined Behavior

答案 2 :(得分:3)

首先,有drop flags - 用于跟踪已初始化哪些变量的运行时信息。如果未分配变量,则不会为其执行drop()

在stable中,drop标志当前存储在类型本身中。向其中写入未初始化的内存可能会导致是否将调用drop()的未定义行为。这很快就会过时,因为每晚都会将drop标志移出类型本身。

在夜间Rust中,如果将未初始化的内存分配给变量,则可以安全地假设将执行drop()。但是,drop()的任何有用实现都将对值进行操作。在Drop特征实现中无法检测类型是否已正确初始化:它可能导致尝试释放无效指针或任何其他随机事物,具体取决于Drop实现的Drop类型。无论如何,将未初始化的内存分配给SecByteBlock的类型是不明智的。