无法恢复的错误与 panic!
有时你的代码中会发生严重问题,而你无能为力。在这些情况下,Rust 提供了 panic! 宏。实际上,有两种方式会导致 panic:一种是执行某个操作使代码产生 panic(例如访问数组越界),另一种是显式调用 panic! 宏。无论哪种情况,都会在程序中引发 panic。默认情况下,这些 panic 会打印失败信息、展开栈帧、清理堆栈并退出程序。通过设置环境变量,你还可以让 Rust 在发生 panic 时显示调用栈,以便更容易定位问题源头。
响应 Panic 的栈展开或终止
默认情况下,当发生 panic 时,程序开始进行栈展开,也就是 Rust 会沿着调用链向上回溯,并清理每个函数中的数据。然而,回溯和清理工作量较大。因此,Rust 允许你选择立即终止程序作为替代方案,即不进行任何清理直接结束运行。
此时程序占用的内存将由操作系统负责回收。如果你的项目需要尽可能减小生成的二进制文件体积,可以通过在 Cargo.toml 文件相应的 [profile] 部分添加 panic = 'abort'
来切换为遇到 panic 时直接终止。例如,如果想要在发布模式下遇到panic就终止,可以这样写:
[profile.release]
panic = 'abort'
下面我们尝试在一个简单程序中调用 panic!
:
文件名:src/main.rs
fn main() {panic!("crash and burn");
}
运行该程序,会看到类似如下输出:
$ cargo runCompiling panic v0.1.0 (file:///projects/panic)Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25sRunning `target/debug/panic`thread 'main' panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
对 panic!
的调用导致最后两行显示错误信息。第一行展示了我们的panic消息以及出错位置:src/main.rs:2:5 表示这是 src/main.rs 文件第 2 行,第 5 个字符处。
这里指示的位置属于我们的代码,如果跳转至该行,就能看到触发宏调用的那条语句。但有时候,panic! 调用可能是在被我们代码间接调用的其他库代码里,此时错误消息报告的是那个库文件及其对应行号,而非最终导致该宏被触发的我们自己的源码位置。
我们可以利用产生这个panic!调用函数链上的回溯来找出引起问题的具体部分。为了理解如何使用这种backtrace,我们来看另一个例子,当因为我们的bug而从库内部触发了一个panic!而不是直接从自己代码里调动宏时,会是什么样子。列表9-1包含了一段尝试访问 vector 中超出有效索引范围元素的代码示例。
这里,我们试图访问向量的第100个元素(索引为99,因为索引从0开始),但该向量只有三个元素。在这种情况下,Rust 会发生 panic。使用 [] 应该返回一个元素,但如果传入了无效的索引,Rust 无法返回正确的元素。在 C 语言中,尝试读取数据结构末尾之外的数据是未定义行为。你可能会得到内存中对应那个位置的数据,即使那块内存不属于该数据结构。这被称为缓冲区溢出读取,如果攻击者能够操纵索引以读取他们不应该访问的数据,就可能导致安全漏洞。为了保护程序免受此类漏洞影响,如果你尝试读取不存在的索引处的元素,Rust 会停止执行并拒绝继续运行。我们来试试看:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/panic`
thread 'main' panicked at src/main.rs:4:6: index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
这个错误指向 main.rs 的第4行,我们在这里尝试访问向量 v 中的索引99。
note 行告诉我们可以设置 RUST_BACKTRACE 环境变量来获取导致错误发生时的回溯信息。回溯是一系列调用函数列表,用于追踪到达当前点所经过的所有函数调用。Rust 中的回溯与其他语言类似:阅读回溯时应从顶部开始,一直读到看到自己编写文件的位置,那就是问题起源所在的位置。上方代码是你的代码调用过的,下方代码则是调用你的代码。这些上下文可能包括 Rust 核心代码、标准库或你使用的一些 crate。
让我们通过将 RUST_BACKTRACE 环境变量设置为非零值来获取回溯,如下所示:
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6: index out of bounds: the len is 3 but the index is 99
stack backtrace:
0: rust_begin_unwind at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/std/src/panicking.rs:692:5
1: core::panicking::panic_fmt at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:75:14
2: core::panicking::panic_bounds_check at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:273:5
3:<usize as core::slice::index::SliceIndex<[T]>>::index at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs :274 :10
4:core :: slice :: index::<impl core :: ops :: index :: Index<I> for [T]>::index 在 file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index .rs :16 :9
...
note:部分细节已省略,可通过设置 `RUST_BACKTRACE=full` 获取详细完整回溯。
清单9-2:当环境变量 RUST_BACKTRACE 设置后,由 panic! 调用生成并显示出的回溯信息
输出内容非常多!具体输出可能因操作系统和 Rust版本不同而异。要获得带有这些信息的回溯,需要启用调试符号。当使用 cargo build 或 cargo run 且未加 --release 标志时,会默认启用调试符号,这正是我们的情况。
在清单9-2中的输出里,第6行指向项目中导致问题的位置——src/main.rs 文件第4行。如果不希望程序 panic,应从第一条提及自己编写文件路径的信息开始调查。在清单9-1中,我们故意写了会触发 panic 的代码,要修复它,只需避免请求超出向量范围之外的元素即可。当未来你的代码出现 panic 时,你需要弄明白是什么操作、什么数值导致了 panic,以及应该如何改正这段逻辑。
我们将在“是否应该使用 panic!”章节中进一步讨论何时以及为何应或不应使用 panic! 来处理错误情况。