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 ===");
}
似乎总是执行正确的析构函数,而忽略另一个析构函数。如果我尝试使用a
或b
(例如a.disp()
而不是t.disp()
),则会错误地说我可能正在使用未初始化的内存。让我感到惊讶的是panic
王,无论i
的价值是什么,它总是运行正确的析构函数(打印预期的字符串)。
这是怎么发生的?如果运行时可以确定要运行哪个析构函数,那么是否需要为实现Drop
的类型初始化有关内存的部分是否应从上面链接的mem::uninitialized()
文档中删除?
答案 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>());
}
在堆栈标志之前分别打印0
和1
,在堆栈标志之前打印0
和0
。
然而,使用mem::uninitialized
仍然是不安全的,因为编译器仍然可以看到a
变量的赋值并设置drop标志。因此,析构函数将在未初始化的内存上调用。请注意,在您的示例中,Drop
impl不会访问您的类型的任何内存(drop flag除外,但对您来说是不可见的)。因此,您无法访问未初始化的内存(无论如何都是零字节,因为您的类型是零大小的结构)。据我所知,这意味着您的unsafe { std::mem::uninitialized() }
代码实际上是安全的,因为之后不会出现内存不安全的情况。
答案 1 :(得分:17)
这里隐藏着两个问题:
mem::uninitialized()
初始化会导致未定义的行为?让我们按顺序解决它们。
编译器如何跟踪哪个变量被初始化?
编译器为范围结束时必须运行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
的类型是不明智的。