使用 panic! 的不可恢复错误

Unrecoverable Errors with panic!

某些时候,在代码中不好的事情发生了,而对其无计可施。在这些情形下,Rust 有着 panic! 宏。在 panic! 宏执行时,程序就会打印一条失败消息,释放(unwind)并清理掉栈,并在随后退出。在侦测到某种类别的代码错误,且在编写程序时刻,尚不清楚怎样处理这个故障时,就会触发一个程序中止(invoke a panic)。

对程序终止进行响应的栈解除或栈终止(Unwinding the Stack or Aborting in Response to a Panic)

默认情况下,在程序终止发生时,程序就开始 解除栈(unwinding),这是指 Rust 对栈进行回退,并清理他所遇到的各个函数的数据。然而,这样的回退与清理,是很多的工作量。那么因此 Rust 就允许编程者选择立即 终止(aborting) 的替代方案,该替代方案会不加清理的结束程序。程序曾用过的内存,这时就需要由操作系统来清理。如过在项目中,需要将生成的二进制执行文件构造得尽可能小,你们就可以通过把 panic= 'abort',添加到 Cargo.toml 文件中恰当的 [profile] 小节,而从程序中止的栈解除切换为立即终止。比如,若想要在发布模式中,于程序中止时立即终止,那么就要添加这个:

[profile.release]
panic = 'abort'

下面就来在一个简单程序中,尝试调用 panic! 宏:

文件名:src/main.rs

fn main() {
    panic! ("崩溃并燃烧");
}

在运行该程序时,就会看到下面这样的东西:

$ cargo run
   Compiling error_handling_demo v0.1.0 (/home/lenny/rust-lang/error_handling_demo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.40s
     Running `target/debug/error_handling_demo`
thread 'main' panicked at '崩溃并燃烧', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

这个到 panic! 的调用,引起了包含在最后两行中的错误消息。第一行给出了程序中止消息及源码里程序中止发生的位置:src/main.rs:2:5 表示是在这里的 src/main.rs 文件的第二行、第五个字符处。

在此情况下,所指出的那行,就是这里代码的一部分,而在前往到那行时,就会看到那个 panic! 宏调用。在别的情况下,panic! 调用可能会在所编写代码调用的代码中,那么由该错误消息报告出的文件名与行号,就会是 panic! 被调用所在之处的其他人的代码,而不会是最终引起那个 panic! 调用的自己编写的代码行。这里可以使用该 panic! 调用来自那些函数的回溯,来弄清楚此处代码的哪个部分导致了该问题。接下来就要详细讨论这种回溯。

运用 panic! 回溯

现在来看一下另一个示例,看看由于代码中的编码错误,而非由于在代码中直接调用 panic! 宏时,来自库的 panic! 调用到底会是什么样子。下面清单 9-1 有一些尝试访问某个矢量中超出了有效索引范围索引的代码。

文件名:src/main.rs

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

    v[99];
}

清单 9-1:尝试访问某个超出了矢量末端的元素,这会导致一个到 panic! 的调用

这里正尝试访问这里矢量的第 100 个元素(由于索引开始于零处,故那是在索引 99 处),但这个矢量只有 3 个元素。在此情况下,Rust 就会中止。使用 [] 被认为是要返回一个元素的,但在传递某个无效索引时,这里就没有 Rust 可返回的正确元素。

在 C 语言中,尝试读取超出某种数据结构,属于未定义的行为。那么就可以会得到与该数据结构中元素对应的、内存中那个位置处的任何东西,即便该内存不属于那个数据结构。这就叫做 缓冲区重读取(a buffer overread),并能在攻击者可以这样的方式操作索引,来读取存储在该数据结构之后的、本不应允许他们读取的数据时,导致安全漏洞。

Rust 为保护程序免受这类漏洞的危害,就会在尝试位于某个不存在索引处的元素时,停止程序的执行而拒绝继续下去。来尝试运行一下上面的代码看看:

$ cargo run                                                                                                            lenny@vm-manjaro
   Compiling error_handling_demo v0.1.0 (/home/lenny/rust-lang/error_handling_demo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/error_handling_demo`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

此错误指出了,在这里 main.rs 的第 4 行,其中尝试访问索引 99 处。接下来的注解行,讲到这里可将 RUST_BACKTRACE 环境变量,设置为获取究竟发生什么,才导致了这个错误。所谓 回溯(a backtrace),即为到此已调用的全部函数的清单。Rust 中的回溯,与其他语言中的回溯完成的事情一样:阅读回溯的冠军,就是要从顶部开始,一直要读到自己编写的文件为止。那便是该问题缘起之处。在那个点位之上的那些行,就是所编写代码曾调用过的代码;而所编写代码之下的那些行,则是调用所编写代码的代码。这些前前后后的行,就可能包含核心 Rust 代码、 标准库代码,或者正使用着的代码箱。下面就来通过将 RUST_BACKTRACE 环境变量,设置为除 0 之外的任何值,尝试获取到回溯。下面清单 9-2 展示了与将会看到的类似输出。

$ RUST_BACKTRACE=1 cargo run                                                                                           lenny@vm-manjaro
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/error_handling_demo`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5
   1: core::panicking::panic_fmt
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14
   2: core::panicking::panic_bounds_check
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9
   6: error_handling_demo::main
             at ./src/main.rs:4:5
   7: core::ops::function::FnOnce::call_once
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

清单 9-2:在设置了 RUST_BACKTRACE 环境变量时,由到 panic! 调用生成的回溯被显示了出来

那可是很多的输出了!具体看到的输出,可能根据操作系统与 Rust 版本而有所不同。为从此信息中获得回溯,就要开启那些调试符号。在不带 --release 标志使用 cargo buildcargo run 时,如同这里这样,这些调试符号默认就是开启的。

在上面清单 9-2 里的输出中,回溯所指向到这里项目中行的第 6 行,就是导致问题的行:即 src/main.rs 的第 4 行。在不想要这个程序中止时,就应在首个提到了自己所编写文件的行,所指向的那个位置,开始排查。在之前的清单 9-1 中,那里有意编写了会中止的代码,而修正程序中止的办法,就是不要请求某个超出那个矢量索引范围的元素。而在今后代码中止时,就需要搞清楚代码是在对什么值进行何种操作,而导致了中止,以及代码应该怎么做。

在本章的 panic! 或不要 panic! 小节,将回到 panic! 这个话题,并讨论在何时要用 panic!,何时不应使用 panic! 来处理不同错误情形。接下来,就会看看怎样使用 Result,从错误中恢复过来。

Last change: 2023-11-30, commit: 6aef347