写点什么

Rust 从 0 到 1- 错误处理 -panic!

用户头像
关注
发布于: 2021 年 05 月 19 日
Rust从0到1-错误处理-panic!

Rust 对错误的处理方式同样延续了其可靠性的执着。错误对于软件来说就像是客观存在的,无法避免。因此 Rust 自然也为此做了准备,以更好的应对发生错误的情况。通常 Rust 要求我们首先要知道出错的可能性,并在编译代码之前就处理错误的情况。通过在我们将代码部署到生产环境之前就发现错误并正确地处理它们会使得我们的程序更为健壮!

Rust 将错误主要分为两个类别:可恢复错误(recoverable)和 不可恢复错误(unrecoverable)。我理解可恢复错误的含义是这个错误并不是程序的问题,可能是某些依赖条件未满足,可以在不修改程序的情况下解决,譬如,文件未找到,或网络不可访问;而不可恢复错误通常是程序的问题,一般就是 bug,譬如,数组越界错误。

其它大部分语言并未对错误进行这种区分,而是采用统一称作“错误”或“异常”。Rust 中没有异常(exception)的概念,只有可恢复错误 Result<T, E> ,和不可恢复错误 panic!。下面我们首先介绍不可恢复错误 panic! 。

Rust 提供了 panic! 宏用于处理不可恢复错误。当执行这个宏时,程序会打印错误信息,展开(unwind,参见下面的解释)并清理栈数据,接着退出。就像我们前面说的出现这种情况通常是程序的 bug,在不修改程序的前提下,一般无法解决。


发生 panic 时对应的栈展开(unwind stack)或终止处理

当发生错误执行 panic 时,默认情况下程序会开始展开(unwinding),即 Rust 会回溯栈并清理遇到的每个函数所使用的内存数据,这个过程有时候工作量会很大,很消耗性能。

还有,另一种选择,就是是直接终止(abort),即不清理数据程序直接退出。程序所使用的内存交由操作系统来清理。

如果我们希望最终的二进制文件越小越好,可以通过在 Cargo.toml 的 [profile] 部分增加 panic = 'abort' 配置,将对 panic 的处理改为终止。例如,在 release 模式下发生 panic 时直接终止:


[profile.release]

panic = 'abort'


下面让我们直接调用 panic! 看看:

fn main() {    panic!("crash and burn");}
复制代码

在控制台会出现类似这样的输出:

$ cargo run   Compiling panic v0.1.0 (file:///projects/panic)    Finished dev [unoptimized + debuginfo] target(s) in 0.25s     Running `target/debug/panic`thread 'main' panicked at 'crash and burn', src/main.rs:2:5note: Run with `RUST_BACKTRACE=1` for a backtrace.
复制代码

最后两行是关于 panic! 的错误信息。第一行是相关提示信息及源码中 panic 出现的具体位置:src/main.rs:2:5 ,即 src/main.rs 文件的第二行第五个字符。

在这个例子中,发生错误的位置是在我们的代码中,即我们在 main 函数中调用 panic! 宏的地方。而有些时候,panic! 可能会发生在我们的代码所调用的外部代码中。错误信息所提示的位置信息可能指向别人的代码。我们可以使用 backtrace 来追踪具体我们是在代码的哪一处对其进行了调用。

使用 Backtrace

下面让我们来看看另一个例子,不是在我们的代码中发生的 panic! ,譬如,通过索引访问 vector 中的元素越界:

fn main() {    let v = vec![1, 2, 3];    v[99];}
复制代码

上面的例子中,程序尝试访问 v 的第一百个元素(索引从 0 开始),但是它只有三个元素。v[99] 应当返回一个元素,不过由于传递了一个无效索引,就没有可供 Rust 返回的正确的元素。这种情况下程序会发生错误,并执行 panic! 。

在 C 语言里尝试进行越界访问,会产生不确定的行为。你可能会得到其在内存中对应位置存储的任何内容,即使它并不属于相应的变量。这也被称作缓冲区溢出(buffer overread),并可能会引发安全问题,攻击者可以利用这种方式来读取其它存储在内存中的数据,特别是我们希望被保护的数据。

为了保护程序不受这类漏洞影响,如果我们尝试对 vector 进行越界访问,Rust 会通过报错来拒绝并停止执行:

$ cargo run   Compiling panic v0.1.0 (file:///projects/panic)    Finished dev [unoptimized + debuginfo] target(s) in 0.27s     Running `target/debug/panic`thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
复制代码

官网中原文是说:提示信息指向 libcore/slice/mod.rs ,这不是我们编写的代码,而是 Rust 核心库中的代码,真正发生 panic! 的位置(应该是个错误,可能是后来 Rust 做了优化,直接提示我们代码中错误的位置,但是文档没有更新)。

Rust 提醒我们可以通过设置环境变量 RUST_BACKTRACE 来获得 backtrace。backtrace 是从开始执行到发生 panic! 的位置的所有被调用的函数栈列表(类似 Java 的 stacktrace)。而阅读 backtrace 的关键是从头开始找到我们编写的文件位置,一般这就是问题的根源所在,从这一行往上(更小序号)是我们的代码所调用的代码;往下(更大序号)则是调用我们代码的代码。这些外部代码可能是 Rust 核心库代码、标准库代码或来自 crate.io 的代码等等,总之不是我们写的代码:

$ RUST_BACKTRACE=1 cargo runthread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5stack backtrace:   0: rust_begin_unwind             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/std/src/panicking.rs:483   1: core::panicking::panic_fmt             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:85   2: core::panicking::panic_bounds_check             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/panicking.rs:62   3: <usize as core::slice::index::SliceIndex<[T]>>::index             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:255   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/slice/index.rs:15   5: <alloc::vec::Vec<T> as core::ops::index::Index<I>>::index             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/alloc/src/vec.rs:1982   6: panic::main             at ./src/main.rs:4:5   7: core::ops::function::FnOnce::call_once             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ops/function.rs:227note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
复制代码

输出的内容多了很多!实际看到的输出可能会因操作系统和 Rust 版本不同而有所不同。另外还有一个前提是,必须启用 debug 标识(在不使用 --release 参数运行 cargo build 或 cargo run 时默认会启用 debug 标识)。

上例中 backtrace 的第 6 行指向了例子中造成问题的行:src/main.rs 的第 4 行,我们可以从这里开始排查代码的错误,为什么会引起 panic ,同时提示信息 'index out of bounds: the len is 3 but the index is 99' 也很有帮助,会尽可能提示我们发生错误的原因,为我们解决问题提供线索和参考。backtrace 通常会是将来我们用来排查代码错误的主要手段,就像我们在 Java 里使用 stacktrace 排查错误一样。

发布于: 2021 年 05 月 19 日阅读数: 9
用户头像

关注

公众号"山 顽石"欢迎大家关注;) 2021.03.23 加入

IT老兵,关注Rust、Java、前端、架构、大数据、AI、算法等;平时喜欢美食、旅游、宠物、健身、篮球、乒乓球等。希望能和大家交流分享。

评论

发布
暂无评论
Rust从0到1-错误处理-panic!