【问题标题】:Why does Rust allow calling functions via null pointers?为什么 Rust 允许通过空指针调用函数?
【发布时间】:2020-11-19 17:09:30
【问题描述】:

我在 Rust 中试验了函数指针魔术,最后得到了一个代码 sn-p,我完全无法解释它为什么编译,甚至更多,为什么它运行。

fn foo() {
    println!("This is really weird...");
}

fn caller<F>() where F: FnMut() {
    let closure_ptr = 0 as *mut F;
    let closure = unsafe { &mut *closure_ptr };
    closure();
}

fn create<F>(_: F) where F: FnMut() {
    caller::<F>();
}

fn main() {
    create(foo);
    
    create(|| println!("Okay..."));
    
    let val = 42;
    create(|| println!("This will seg fault: {}", val));
}

我无法解释为什么通过将caller(...) 中的空指针转换为F 类型的实例来调用foo。我原以为只能通过相应的函数指针调用函数,但鉴于指针本身为空,显然情况并非如此。话虽如此,看来我显然误解了 Rust 类型系统的一个重要部分。

Example on Playground

【问题讨论】:

  • 话虽如此,看来我显然误解了 Rust 类型系统的一个重要部分。 => 并非如此。未定义的行为意味着任何事情都可能发生,包括不时获得您期望的行为。
  • 可能只是因为您选择了 42。

标签: rust


【解决方案1】:

这个程序根本没有真正构造函数指针——它总是直接调用foo 和这两个闭包。

每个 Rust 函数,无论是闭包还是 fn 项,都有唯一的匿名类型。此类型根据需要实现 Fn/FnMut/FnOnce 特征。 fn 项目的匿名类型是零大小的,就像没有捕获的闭包类型一样。

因此,表达式create(foo) 用foo 的类型实例化create 的参数F——这不是函数指针类型fn(),而是一个匿名的、零大小的类型,仅用于@ 987654336@。在错误消息中,rustc 将此类型称为fn() {foo},如您所见this error message。

在create::&lt;fn() {foo}&gt; 内部(使用错误消息中的名称),表达式caller::&lt;F&gt;() 将此类型转发给caller,但不为其指定该类型的值。

最后,在caller::&lt;fn() {foo}&gt; 中,表达式closure() 脱糖为FnMut::call_mut(closure)。因为closure 具有类型&amp;mut F,其中F 只是零大小的类型fn() {foo},所以closure 本身的0 值根本就不会被使用1,并且程序直接拨打foo。

同样的逻辑适用于闭包|| println!("Okay..."),就像foo 有一个匿名的零大小类型,这次叫做[closure@src/main.rs:2:14: 2:36]。

第二个闭包就没那么幸运了——它的类型不是零大小的,因为它必须包含对变量val的引用。这一次,FnMut::call_mut(closure) 实际上需要取消引用closure 才能完成它的工作。所以它崩溃了2。


1 像这样构造空引用在技术上是未定义的行为,因此编译器不承诺该程序的整体行为。但是,将0 替换为具有F 对齐的其他“地址”可以避免fn() {foo} 等零大小类型的问题,并给出the same behavior!)

2 同样,构造一个空(或悬空)引用是这里实际承担责任的操作——之后,任何事情都会发生。段错误只是一种可能性——未来版本的 rustc,或者在稍微不同的程序上运行的相同版本,可能会完全做其他事情!

【讨论】:

  • 未来读者请注意:如果您真的想实现对零大小F 的悬空引用,NonNull::&lt;F&gt;::dangling() 比align_of::&lt;F&gt;() as *mut F 更好。它做同样的事情,但犯错误导致不健全的可能性较小。 (为了便于说明,align_of 效果更好。)
【解决方案2】:

type of fn foo() {...} 不是函数指针fn(),它实际上是foo 特有的唯一类型。只要您携带该类型(此处为F),编译器就知道如何在不需要任何额外指针的情况下调用它(这种类型的值不携带数据)。不捕获任何内容的闭包以相同的方式工作。只有当最后一个闭包尝试查找 val 时,它才会变得冒险,因为您将 0 放置在(可能)指向 val 的指针应该在的位置。

您可以使用size_of 观察到这一点,在前两次调用中,closure 的大小为零,但在最后一次调用闭包中捕获的内容时,大小为 8(至少在操场上) .如果大小为 0,则程序不必从 NULL 指针加载任何内容。

NULL 指向引用的指针的有效转换仍然是未定义的行为,但因为类型恶作剧而不是因为内存访问恶作剧:拥有真正为 NULL 的引用本身是非法的,因为像Option&lt;&amp;T&gt; 这样的类型依赖于引用的值永远不是NULL 的假设。这是一个如何出错的示例:

unsafe fn null<T>(_: T) -> &'static mut T {
    &mut *(0 as *mut T)
}

fn foo() {
    println!("Hello, world!");
}

fn main() {
    unsafe {
        let x = null(foo);
        x(); // prints "Hello, world!"
        let y = Some(x);
        println!("{:?}", y.is_some()); // prints "false", y is None!
    }
}

【讨论】:

    【解决方案3】:

    鉴于 rust 是建立在 LLVM 之上的,并且您所做的是保证 UB,您可能会遇到类似于 https://kristerw.blogspot.com/2017/09/why-undefined-behavior-may-call-never.html 的内容。这是安全防锈能够消除所有 UB 的众多原因之一。

    【讨论】:

    • 此版本具有未定义的行为,但将0 as *mut F 更改为mem::align_of::&lt;F&gt;() as *mut F 使其定义为对create 的前两次调用。在不引用 UB 的情况下更好地解释该行为。
    • 不管怎样都是UB,空指针到引用的转换是UB,所以整个程序是不健全的。调用`create
    • 我认为@rpjohnst 试图使其成为非空指针,尽管我不确定他为什么不使用任意整数而不是调用mem::align_of。但它仍然是 UB,因为有效的 F 不在那个地址...
    • 对于前两个调用,F 的大小为零,并且 每个 处都有一个有效的零大小值 非零对齐的地址。此模式在标准库中部分编码为NonNull::dangling
    • 在您提供的链接中,LLVM 错误地假设地址 0x0 永远不能指向有效的函数对象,这在 x86 上是一个无效的假设,因为 0x0 可能被内存映射内核。 C99 标准没有明确禁止以任何方式调用/访问地址0x0,只要它包含有效的指令/数据。 0x0 未映射的假设是合理的,但是当它确实是 LLVM 中的一个缺陷时,将其引用为 C 标准中的一个缺陷是不公平的。
    【解决方案4】:

    虽然这完全取决于 UB,但我认为在这两种情况下可能会发生以下情况:

    1. F 类型是一个没有数据的闭包。这相当于一个函数,也就是说F是一个函数项。这意味着编译器可以将任何对F 的调用优化为对生成F 的任何函数的调用(无需创建函数指针)。有关这些事物的不同名称的示例,请参见 this。

    2. 编译器看到val 总是42,因此它可以将它优化为一个常量。如果是这种情况,那么传递给create 的闭包又是一个没有捕获项目的闭包,因此我们可以遵循#1 中的想法。

    另外,我说这是 UB,但是请注意一些关于 UB 的关键:如果您调用 UB 并且编译器以意想不到的方式利用它,它并不是试图搞砸你,它正在尝试优化您的代码。毕竟,UB 是关于编译器错误优化的事情,因为你已经打破了它的一些期望。因此,编译器以这种方式优化是完全合乎逻辑的。编译器不以这种方式优化而是利用 UB 也是完全合乎逻辑的。

    【讨论】:

    • 这种行为可以在不参考任何优化的情况下解释 - 它是 Rust 如何静态调度特征方法的函数,即使从程序中删除所有 UB 也可以看到。
    【解决方案5】:

    这是“工作”,因为fn() {foo} 和第一个闭包是零大小的类型。扩展答案:

    如果这个程序最终在 Miri(未定义的行为检查器)中执行,它最终会失败,因为 NULL 指针被取消引用。 NULL 指针永远不能被取消引用,即使对于零大小的类型也是如此。但是,未定义的行为可以做任何事情,因此编译器不会对行为做出任何承诺,这意味着它可能会在未来的 Rust 版本中中断。

    error: Undefined Behavior: memory access failed: 0x0 is not a valid pointer
      --> src/main.rs:7:28
       |
    7  |     let closure = unsafe { &mut *closure_ptr };
       |                            ^^^^^^^^^^^^^^^^^ memory access failed: 0x0 is not a valid pointer
       |
       = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
       = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
               
       = note: inside `caller::<fn() {foo}>` at src/main.rs:7:28
    note: inside `create::<fn() {foo}>` at src/main.rs:13:5
      --> src/main.rs:13:5
       |
    13 |     func_ptr();
       |     ^^^^^^^^^^
    note: inside `main` at src/main.rs:17:5
      --> src/main.rs:17:5
       |
    17 |     create(foo);
       |     ^^^^^^^^^^^
    

    这个问题可以通过写let closure_ptr = 1 as *mut F; 轻松解决,然后它只会在第 22 行失败,第二个闭包会出现段错误。

    error: Undefined Behavior: inbounds test failed: 0x1 is not a valid pointer
      --> src/main.rs:7:28
       |
    7  |     let closure = unsafe { &mut *closure_ptr };
       |                            ^^^^^^^^^^^^^^^^^ inbounds test failed: 0x1 is not a valid pointer
       |
       = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
       = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
               
       = note: inside `caller::<[closure@src/main.rs:22:12: 22:55 val:&i32]>` at src/main.rs:7:28
    note: inside `create::<[closure@src/main.rs:22:12: 22:55 val:&i32]>` at src/main.rs:13:5
      --> src/main.rs:13:5
       |
    13 |     func_ptr();
       |     ^^^^^^^^^^
    note: inside `main` at src/main.rs:22:5
      --> src/main.rs:22:5
       |
    22 |     create(|| println!("This will seg fault: {}", val));
       |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    

    为什么它没有抱怨foo 或|| println!("Okay...")?好吧,因为它们不存储任何数据。当引用一个函数时,您不会得到一个函数指针,而是一个代表该特定函数的零大小类型 - 这有助于单态化,因为每个函数都是不同的。可以从对齐的悬空指针创建不存储任何数据的结构。

    但是,如果您通过 create::&lt;fn()&gt;(foo) 明确表示该函数是函数指针,那么程序将停止工作。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-02-12
      • 1970-01-01
      • 2020-11-03
      • 2011-06-26
      • 1970-01-01
      • 1970-01-01
      • 2011-08-20
      相关资源
      最近更新 更多