panic!与不可恢复的错误

ch09-01-unrecoverable-errors-with-panic.md
commit 380e6ee57c251f5ffa8df4c58b3949405448d914

突然有一天,糟糕的事情发生了,而你对此束手无策。对于这种情况,Rust 有`panic!宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,并接着退出。出现这种情况的场景通常是检测到一些类型的 bug 而且程序员并不清楚该如何处理它。

Panic 中的栈展开与终止

当出现panic!时,程序默认会开始展开unwinding),这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接终止abort),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。如果你需要项目的最终二进制文件越小越好,可以由 panic 时展开切换为终止,通过在 Cargo.toml[profile]部分增加panic = 'abort'。例如,如果你想要在发布模式中 panic 时直接终止:

[profile.release]
panic = 'abort'

让我们在一个简单的程序中调用panic!

Filename: src/main.rs

fn main() {
    panic!("crash and burn");
}

运行程序将会出现类似这样的输出:

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished debug [unoptimized + debuginfo] target(s) in 0.25 secs
     Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)

最后三行包含panic!造成的错误信息。第一行显示了 panic 提供的信息并指明了源码中 panic 出现的位置:src/main.rs:2 表明这是 src/main.rs 文件的第二行。

在这个例子中,被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现panic!宏的调用。换句话说,panic!可能会出现在我们的代码调用的代码中。错误信息报告的文件名和行号可能指向别人代码中的panic!宏调用,而不是我们代码中最终导致panic!的那一行。可以使用panic!被调用的函数的 backtrace 来寻找(我们代码中出问题的地方)。

使用panic!backtrace

让我们来看看另一个因为我们代码中的 bug 引起的别的库中panic!的例子,而不是直接的宏调用:

Filename: src/main.rs

fn main() {
    let v = vec![1, 2, 3];

    v[100];
}

这里尝试访问 vector 的第一百个元素,不过它只有三个元素。这种情况下 Rust 会 panic。[]应当返回一个元素,不过如果传递了一个无效索引,就没有可供 Rust 返回的正确的元素。

这种情况下其他像 C 这样语言会尝直接试提供所要求的值,即便这可能不是你期望的:你会得到对任何应 vector 中这个元素的内存位置的值,甚至是这些内存并不属于 vector 的情况。这被称为缓冲区溢出buffer overread),并可能会导致安全漏洞,比如攻击者可以像这样操作索引来读取储存在数组后面不被允许的数据。

为了使程序远离这类漏洞,如果尝试读取一个索引不存在的元素,Rust 会停止执行并拒绝继续。尝试运行上面的程序会出现如下:

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished debug [unoptimized + debuginfo] target(s) in 0.27 secs
     Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is
100', /stable-dist-rustc/build/src/libcollections/vec.rs:1362
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)

这指向了一个不是我们编写的文件,libcollections/vec.rs。这是标准库中Vec<T>的实现。这是当对 vector v使用[]libcollections/vec.rs 中会执行的代码,也是真正出现panic!的地方。

接下来的几行提醒我们可以设置RUST_BACKTRACE环境变量来得到一个 backtrace 来调查究竟是什么导致了错误。让我们来试试看。列表 9-1 显示了其输出:

$ RUST_BACKTRACE=1 cargo run
    Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is
100', /stable-dist-rustc/build/src/libcollections/vec.rs:1395
stack backtrace:
   1:        0x10922522c -
std::sys::imp::backtrace::tracing::imp::write::h1204ab053b688140
   2:        0x10922649e -
std::panicking::default_hook::{{closure}}::h1204ab053b688140
   3:        0x109226140 - std::panicking::default_hook::h1204ab053b688140
   4:        0x109226897 -
std::panicking::rust_panic_with_hook::h1204ab053b688140
   5:        0x1092266f4 - std::panicking::begin_panic::h1204ab053b688140
   6:        0x109226662 - std::panicking::begin_panic_fmt::h1204ab053b688140
   7:        0x1092265c7 - rust_begin_unwind
   8:        0x1092486f0 - core::panicking::panic_fmt::h1204ab053b688140
   9:        0x109248668 -
core::panicking::panic_bounds_check::h1204ab053b688140
  10:        0x1092205b5 - <collections::vec::Vec<T> as
core::ops::Index<usize>>::index::h1204ab053b688140
  11:        0x10922066a - panic::main::h1204ab053b688140
  12:        0x1092282ba - __rust_maybe_catch_panic
  13:        0x109226b16 - std::rt::lang_start::h1204ab053b688140
  14:        0x1092206e9 - main

Listing 9-1: The backtrace generated by a call to panic! displayed when the environment variable RUST_BACKTRACE is set

这里有大量的输出!backtrace 第 11 行指向了我们程序中引起错误的行:src/main.rs 的第四行。backtrace 是一个执行到目前位置所有被调用的函数的列表。Rust 的 backtrace 跟其他语言中的一样:阅读 backtrace 的关键是从头开始读直到发现你编写的文件。这就是问题的发源地。这一行往上是你的代码调用的代码;往下则是调用你的代码的代码。这些行可能包含核心 Rust 代码,标准库代码或用到的 crate 代码。

如果你不希望我们的程序 panic,第一个提到我们编写的代码行的位置是你应该开始调查的,以便查明是什么值如何在这个地方引起了 panic。在上面的例子中,我们故意编写会 panic 的代码来演示如何使用 backtrace,修复这个 panic 的方法就是不要尝试在一个只包含三个项的 vector 中请求索引是 100 的元素。当将来你得代码出现了 panic,你需要搞清楚在这特定的场景下代码中执行了什么操作和什么值导致了 panic,以及应当如何处理才能避免这个问题。

本章的后面会再次回到panic!并讲到何时应该何时不应该使用这个方式。接下来,我们来看看如何使用Result来从错误中恢复。