ust*_*ion 21 destructor stack-unwinding undefined-behavior rust
该文档mem::uninitialized指出了使用该函数危险/不安全的原因:调用drop未初始化的内存是未定义的行为.
因此,我相信这段代码应该是未定义的:
let a: TypeWithDrop = unsafe { mem::uninitialized() };
panic!("=== Testing ==="); // Destructor of `a` will be run (U.B)
Run Code Online (Sandbox Code Playgroud)
但是,我编写了这段代码,它在安全的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 ===");
}
Run Code Online (Sandbox Code Playgroud)
似乎总是执行正确的析构函数,而忽略另一个析构函数.如果我尝试使用a或b(a.disp()而不是t.disp())它正确错误地说我可能正在使用未初始化的内存.让我感到惊讶的是,在panic国王面前,它始终运行正确的析构函数(打印预期的字符串),无论其价值i是多少.
这是怎么发生的?如果运行时可以确定要运行哪个析构函数,那么是否需要为已实现的类型初始化有关内存的部分是否应从上面链接的Drop文档中删除mem::uninitialized()?
oli*_*obk 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>());
}
Run Code Online (Sandbox Code Playgroud)
打印0和1叠前旗分别,并0和0与堆栈标志.
mem::uninitialized但是,使用仍然是不安全的,因为编译器仍然看到对a变量的赋值并设置drop标志.因此,析构函数将在未初始化的内存上调用.请注意,在您的示例中,Dropimpl不会访问您的类型的任何内存(drop flag除外,但对您来说是不可见的).因此,您无法访问未初始化的内存(无论如何都是零字节,因为您的类型是零大小的结构).据我所知,这意味着您的unsafe { std::mem::uninitialized() }代码实际上是安全的,因为之后不会出现内存不安全的情况.
Mat*_* M. 17
这里隐藏着两个问题:
mem::uninitialized()导致未定义的行为导致初始化?让我们按顺序解决它们.
编译器如何跟踪哪个变量被初始化?
编译器注入所谓的"丢弃标志":对于Drop必须在作用域末尾运行的每个变量,在堆栈上注入一个布尔标志,说明是否需要处理该变量.
标志从"no"开始,如果变量初始化则变为"yes",如果变量被移动则变为"no".
最后,当有时间删除此变量时,将检查该标志并在必要时将其删除.
这与编译器的流分析是否抱怨可能未初始化的变量无关:只有在满足流分析时才生成代码.
为什么可能
mem::uninitialized()导致未定义的行为导致初始化?
在使用时mem::uninitialized()你向编译器做出承诺:别担心,我肯定是在初始化它.
就编译器而言,变量因此被完全初始化,并且drop标志被设置为"yes"(直到你离开它为止).
反过来,这意味着Drop将被调用.
使用未初始化的对象是Undefined Behavior,并且Drop代表您调用未初始化对象的编译器计为"使用它".
奖金:
在我的测试中,没有什么奇怪的事发生!
请注意,未定义的行为意味着任何事情都可能发生; 不幸的是,任何事情都包括"似乎工作"(甚至"尽管有可能"按预期工作").
特别是,如果你不在Drop::drop(只是打印)中访问对象的内存,那么一切都可能正常工作.但是,如果你确实访问它,你可能会看到奇怪的整数,指向野外的指针等等......
如果优化器很聪明,即使没有访问它,也可能会做奇怪的事情!由于我们使用的是LLVM,因此我邀请您阅读Chris Lattner(LLVM的父亲)对每个C程序员应该了解的未定义行为的内容.