【问题标题】:How does Rust achieve compile-time-only pointer safety?Rust 如何实现仅编译时指针安全?
【发布时间】:2015-04-14 13:28:45
【问题描述】:

我在某处读到,在具有指针功能的语言中,由于各种原因,编译器不可能在编译时完全确定所有指针是否正确使用和/或是否有效(引用活动对象),因为这基本上构成了解决停机问题。这并不奇怪,直观地说,因为在这种情况下,我们将能够在编译时推断程序的运行时行为,类似于 this related question 中所述。

但是,据我所知,Rust 语言要求指针检查完全在编译时完成(没有与指针相关的未定义行为,至少是“安全”指针,并且没有“无效指针”或“null指针”运行时异常)。

假设 Rust 编译器没有解决停机问题,那么谬误在哪里?

  • 是否指针检查并未完全在编译时完成, 与 C 中的原始指针相比,Rust 的智能指针仍会引入一些运行时开销?
  • 或者,Rust 编译器是否有可能无法做出完全正确的决定,它有时需要 Just Trust The Programmer™,可能使用生命周期注释之一(具有 <'lifetime_ident> 语法的注释)?在这种情况下,这是否意味着指针/内存的安全保证不是 100%,仍然依赖于程序员编写正确的代码?
  • 另一种可能性是,Rust 指针是非“通用的”或在某种意义上受到限制,因此编译器可以在编译时完全推断它们的属性,但它们不如 e 有用。 G。 C 中的原始指针或 C++ 中的智能指针。
  • 或者可能是完全不同的东西,我误解了
    { "pointer", "safety", "guaranteed", "compile-time" } 中的一个或多个。

【问题讨论】:

  • “在具有指针功能的语言中,编译器不可能在编译时完全决定所有指针是否正确使用和/或对于各种指针是否有效(参考活动对象)原因,因为这基本上将构成解决停机问题。”我对这种说法的真实性提出质疑。 C 风格的指针,也许是因为它们缺乏各种好东西,也有各种不好的东西,比如指针算法,但是对于人们真正需要从指针中得到的东西,Rust 及其引用是一个完美的例子。声明。
  • @ChrisMorgan 我明白了,谢谢。

标签: pointers rust memory-safety


【解决方案1】:

免责声明:我有点赶时间,所以这有点曲折。随意清理它。

语言设计师讨厌的一个偷偷摸摸的把戏™ 基本上是这样的:Rust 只能推断'static 的生命周期(用于全局变量和其他整个程序生命周期的东西)和堆栈(局部)变量:它不能表达或推理分配的生命周期。

这意味着几件事。首先,所有处理堆分配的库类型(Box<T>Rc<T>Arc<T>)都拥有它们指向的东西。因此,它们实际上需要个生命周期才能存在。

确实需要生命周期的地方是当您访问智能指针的内容时。例如:

let mut x: Box<i32> = box 0;
*x = 42;

第二行的幕后情况是这样的:

{
    let box_ref: &mut Box<i32> = &mut x;
    let heap_ref: &mut i32 = box_ref.deref_mut();
    *heap_ref = 42;
}

换句话说,因为Box 不是魔法,我们必须告诉编译器如何把它变成一个普通的、普通的借用指针。这就是 DerefDerefMut 特征的用途。这就提出了一个问题:heap_ref 的生命周期究竟是什么?

这个问题的答案在DerefMut的定义中(因为我很着急,凭记忆):

trait DerefMut {
    type Target;
    fn deref_mut<'a>(&'a mut self) -> &'a mut Target;
}

就像我之前说的,Rust绝对不能谈论“堆生命周期”。相反,它必须将堆分配的i32 的生命周期与它现有的唯一另一个生命周期联系起来:Box 的生命周期。

这意味着“复杂”的事物没有可表达的生命周期,因此必须拥有它们管理的事物。当您将复杂的智能指针/句柄转换为简单的借用指针时,是您必须引入生命周期的时刻,而您通常只使用句柄本身的生命周期。

实际上,我应该澄清一下:“句柄的生命周期”,我真正的意思是“当前存储句柄的变量的生命周期”:生命周期实际上是用于存储,而不是。这就是为什么 Rust 新手在无法弄清楚为什么他们不能做类似的事情时会被绊倒的原因:

fn thingy<'a>() -> (Box<i32>, &'a i32) {
    let x = box 1701;
    (x, &x)
}

“但是......我知道盒子会继续存在,为什么编译器会说它不存在?!” 因为 Rust 无法推理堆生命周期和 必须&amp;x的生命周期绑定到变量x而不是它恰好指向的堆分配。

【讨论】:

    【解决方案2】:

    是不是指针检查没有完全在编译时完成,并且与 C 中的原始指针相比,Rust 的智能指针仍然会引入一些运行时开销?

    对于无法在编译时检查的内容,有特殊的运行时检查。这些通常在cell crate 中找到。但总的来说,Rust 在编译时检查所有内容,并且应该生成与 C 中相同的代码(如果您的 C 代码没有执行未定义的内容)。

    或者,Rust 编译器是否有可能无法做出完全正确的决定,并且有时需要 Just Trust The Programmer™,可能使用生命周期注释之一(具有 语法的注释)?这种情况下,是不是意味着指针/内存的安全保证不是100%,还是要靠程序员写正确的代码?

    如果编译器无法做出正确的决定,您会收到一个编译时错误,告诉您编译器无法验证您在做什么。这也可能会限制您使用您知道正确的内容,但编译器不会。在这种情况下,您可以随时访问unsafe 代码。但正如您正确假设的那样,编译器部分依赖于程序员。

    编译器检查函数的实现,看看它是否完全按照生命周期所说的那样做。然后,在函数的调用点,它检查程序员是否正确使用了函数。这类似于类型检查。 C++ 编译器检查您是否返回了正确类型的对象。然后它在调用站点检查返回的对象是否存储在正确类型的变量中。函数的程序员在任何时候都不能违背承诺(除非使用了unsafe,但您始终可以让编译器强制在您的项目中不使用unsafe

    Rust 不断得到改进。一旦编译器变得更智能,更多的东西可能在 Rust 中变得合法。

    另一种可能性是,Rust 指针是非“通用的”或在某种意义上受到限制,因此编译器可以在编译时完全推断它们的属性,但它们不如 e 有用。 G。 C 中的原始指针或 C++ 中的智能指针。

    在 C 中有一些可能出错的地方:

    1. 悬空指针
    2. 双重免费
    3. 空指针
    4. 野指针

    这些不会发生在安全的 Rust 中。

    1. 永远不能有一个指针指向不再位于堆栈或堆上的对象。这在编译时通过生命周期得到证明。
    2. Rust 中没有手动内存管理。使用 Box 分配对象(类似于但不等于 C++ 中的 unique_ptr
    3. 同样,没有手动内存管理。 Boxes 自动释放内存。
    4. 在安全的 Rust 中,您可以创建指向任何位置的指针,但不能取消引用它。您创建的任何引用始终绑定到一个对象。

    在 C++ 中可能会出错:

    1. C 中所有可能出错的地方
    2. 智能指针只会帮助您不要忘记致电free。您仍然可以创建悬空引用:auto x = make_unique&lt;int&gt;(42); auto&amp; y = *x; x.reset(); y = 99;

    Rust 修复了这些:

    1. 见上文
    2. 只要y存在,就不能修改x。这是在编译时检查的,不能被更多级别的间接或结构规避。

    我在某处读到,在具有指针功能的语言中,由于各种原因,编译器不可能在编译时完全确定所有指针是否正确使用和/或是否有效(引用活动对象),因为这基本上可以解决停机问题。

    Rust 并不能证明你所有的指针都被正确使用了。你仍然可以编写虚假程序。 Rust 证明你没有使用无效指针。 Rust 证明你永远不会有空指针。 Rust 证明你永远不会有两个指向同一个对象的指针,除非所有这些指针都是不可变的(const)。 Rust 不允许您编写任何程序(因为这将包括违反内存安全的程序)。目前,Rust 仍然阻止您编写一些有用的程序,但计划允许使用安全的 Rust 编写更多(合法的)程序。

    这并不奇怪,直观地说,因为在这种情况下,我们将能够在编译时推断程序的运行时行为,类似于 this related question 中所述。

    重温您引用的关于停止问题的问题中的示例:

    void foo() {
        if (bar() == 0) this->a = 1;
    }
    

    上面的 C++ 代码看起来是 Rust 中的两种方式之一:

    fn foo(&mut self) {
        if self.bar() == 0 {
            self.a = 1;
        }
    }
    
    fn foo(&mut self) {
        if bar() == 0 {
            self.a = 1;
        }
    }
    

    对于任意的bar,您无法证明这一点,因为它可能会访问全局状态。 Rust 很快就获得了const 函数,可用于在编译时计算东西(类似于constexpr)。如果barconst,那么在编译时证明self.a 是否设置为1 变得很简单。除此之外,没有pure函数或函数内容的其他限制,你永远无法证明self.a是否设置为1

    Rust 目前不关心你的代码是否被调用。它关心分配期间是否仍然存在self.a 的内存。 self.bar() 永远不能破坏 selfunsafe 代码除外)。因此self.a 将始终在if 分支中可用。

    【讨论】:

    • 很好的解释。只有一件事对我来说并不完全清楚:“Rust 不能证明你所有的指针都被正确使用”是什么意思?当然可以编写“虚假”程序,即。 e.那些应该做某事但他们实际上做了一些不同的事情,并给我们错误的结果。通过“正确使用指针”,我的意思是“您不会因无效/空/悬空指针而导致内存错误”。
    • 是的。你仍然可以在你刚读过的整数上写42,然后想知道为什么你输入的整数是 42 并不重要。这是一个逻辑错误,而不是内存错误。
    【解决方案3】:

    Rust 引用的大部分安全性由严格的规则保证:

    • 如果您拥有一个 const 引用 (&amp;),您可以克隆该引用并传递它,但不能从中创建一个可变的 &amp;mut 引用。
    • 如果存在对某个对象的可变 (&amp;mut) 引用,则不能存在对该对象的其他引用。
    • 不允许引用比它所引用的对象寿命长,并且所有操作引用的函数必须声明如何使用生命周期注释来链接来自其输入和输出的引用(如'a) .

    因此,就表达性而言,我们实际上比使用纯原始指针时更受限制(例如,仅使用安全引用无法构建图形结构),但这些规则可以在编译时有效地完全检查。

    然而,仍然可以使用原始指针,但您必须将处理它们的代码包含在 unsafe { /* ... */ } 块中,告诉编译器“相信我,我知道我在这里做什么”。这就是一些特殊的智能指针在内部所做的事情,例如RefCell,它允许您在运行时而不是编译时检查这些规则,以获得表现力。

    【讨论】:

    • “所有操作引用的函数都必须声明来自其输入和输出的引用是如何链接的”——是的,这些正是我所指的生命周期注释。这是否意味着如果我弄错了生命周期注释,我可以取消引用无效指针并崩溃?
    • 不,因为编译器实际上会检查您的注释是否正确。这有时会迫使程序员做一些不寻常的事情,以便编译器看到注释是正确的。
    • 但另一方面,如果你在 unsafe 块中做错了什么,任何事情都可能发生。
    • 当然,unsafe 块中可能发生任何事情,因此我对它们一点也不感兴趣。所以答案是“如果编译器不能证明它是正确的,那就是非法的。”谢谢!
    猜你喜欢
    • 2019-01-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-23
    • 1970-01-01
    相关资源
    最近更新 更多