Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

panic! 处理不可恢复的错误

Unrecoverable Errors with panic!

有时候,代码中会发生一些不好的事情,而你对此无能为力。在这种情况下,Rust 有 panic! 宏。在实践中,有两种方式会导致 panic:执行会导致代码 panic 的操作(例如访问数组越界)或者显式调用 panic! 宏。在这两种情况下,我们都会在程序中引发 panic。默认情况下,这些 panic 会打印一条失败消息,展开并清理栈,然后退出。通过环境变量,你还可以让 Rust 在发生 panic 时显示调用栈,以便更容易地追踪 panic 的来源。

Sometimes bad things happen in your code, and there’s nothing you can do about it. In these cases, Rust has the panic! macro. There are two ways to cause a panic in practice: by taking an action that causes our code to panic (such as accessing an array past the end) or by explicitly calling the panic! macro. In both cases, we cause a panic in our program. By default, these panics will print a failure message, unwind, clean up the stack, and quit. Via an environment variable, you can also have Rust display the call stack when a panic occurs to make it easier to track down the source of the panic.

展开栈或中止以响应 Panic

Unwinding the Stack or Aborting in Response to a Panic

默认情况下,当发生 panic 时,程序开始 展开(unwinding),这意味着 Rust 会回溯栈并清理遇到的每个函数中的数据。然而,回溯和清理工作量很大。因此,Rust 允许你选择立即 中止(aborting)作为替代方案,这会在不进行清理的情况下结束程序。

By default, when a panic occurs, the program starts unwinding, which means Rust walks back up the stack and cleans up the data from each function it encounters. However, walking back and cleaning up is a lot of work. Rust therefore allows you to choose the alternative of immediately aborting, which ends the program without cleaning up.

程序正在使用的内存随后将需要由操作系统进行清理。如果在你的项目中需要使生成的二进制文件尽可能小,你可以通过在 Cargo.toml 文件的适当 [profile] 部分添加 panic = 'abort',将 panic 时的行为从展开切换为中止。例如,如果你想在发布模式下发生 panic 时中止,请添加以下内容:

Memory that the program was using will then need to be cleaned up by the operating system. If in your project you need to make the resultant binary as small as possible, you can switch from unwinding to aborting upon a panic by adding panic = 'abort' to the appropriate [profile] sections in your Cargo.toml file. For example, if you want to abort on panic in release mode, add this:

[profile.release]
panic = 'abort'

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

Let’s try calling panic! in a simple program:

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

当你运行该程序时,你会看到类似以下的内容:

When you run the program, you’ll see something like this:

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
     Running `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 消息以及源代码中发生 panic 的位置:src/main.rs:2:5 表示它是 src/main.rs 文件的第 2 行、第 5 个字符。

The call to panic! causes the error message contained in the last two lines. The first line shows our panic message and the place in our source code where the panic occurred: src/main.rs:2:5 indicates that it’s the second line, fifth character of our src/main.rs file.

在这种情况下,指示的行是我们代码的一部分,如果我们查看该行,就会看到 panic! 宏调用。在其他情况下,panic! 调用可能位于我们代码所调用的代码中,错误消息报告的文件名和行号将是调用 panic! 宏的其他人的代码,而不是最终导致 panic! 调用的我们代码中的行。

In this case, the line indicated is part of our code, and if we go to that line, we see the panic! macro call. In other cases, the panic! call might be in code that our code calls, and the filename and line number reported by the error message will be someone else’s code where the panic! macro is called, not the line of our code that eventually led to the panic! call.

我们可以使用 panic! 调用来源函数的回溯(backtrace)来找出代码中导致问题的部分。为了理解如何使用 panic! 回溯,让我们看另一个例子,看看当 panic! 调用来自库且是由我们代码中的 Bug 而非直接调用宏引起时是什么样子的。示例 9-1 中的代码尝试访问 vector 中超出有效索引范围的索引。

We can use the backtrace of the functions the panic! call came from to figure out the part of our code that is causing the problem. To understand how to use a panic! backtrace, let’s look at another example and see what it’s like when a panic! call comes from a library because of a bug in our code instead of from our code calling the macro directly. Listing 9-1 has some code that attempts to access an index in a vector beyond the range of valid indexes.

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

    v[99];
}

在这里,我们尝试访问 vector 的第 100 个元素(索引为 99,因为索引从零开始),但 vector 只有三个元素。在这种情况下,Rust 会发生 panic。使用 [] 应该返回一个元素,但如果你传递了一个无效索引,Rust 在这里无法返回任何正确的元素。

Here, we’re attempting to access the 100th element of our vector (which is at index 99 because indexing starts at zero), but the vector has only three elements. In this situation, Rust will panic. Using [] is supposed to return an element, but if you pass an invalid index, there’s no element that Rust could return here that would be correct.

在 C 语言中,尝试读取数据结构末尾之外的内容是未定义行为。你可能会得到内存中与数据结构中该元素对应的位置上的任何内容,即使该内存并不属于该结构。这被称为 缓冲区超读(buffer overread),如果攻击者能够操纵索引从而读取存储在数据结构之后的不应被允许访问的数据,则可能导致安全漏洞。

In C, attempting to read beyond the end of a data structure is undefined behavior. You might get whatever is at the location in memory that would correspond to that element in the data structure, even though the memory doesn’t belong to that structure. This is called a buffer overread and can lead to security vulnerabilities if an attacker is able to manipulate the index in such a way as to read data they shouldn’t be allowed to that is stored after the data structure.

为了保护你的程序免受此类漏洞的影响,如果你尝试读取不存在的索引处的元素,Rust 将停止执行并拒绝继续。让我们尝试一下看看:

To protect your program from this sort of vulnerability, if you try to read an element at an index that doesn’t exist, Rust will stop execution and refuse to continue. Let’s try it and see:

$ 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 中 vector 的索引 99 的位置。

This error points at line 4 of our main.rs where we attempt to access index 99 of the vector in v.

note: 行告诉我们可以设置 RUST_BACKTRACE 环境变量来获取导致错误的详细回溯信息。回溯(backtrace)是到达当前点所调用的所有函数的列表。Rust 中的回溯与其他语言中的工作方式相同:阅读回溯的关键是从顶部开始阅读,直到看到你编写的文件。那就是问题起源的地方。该位置之上的行是你的代码调用的代码;之下的行是调用你代码的代码。这些前后的行可能包括 Rust 核心代码、标准库代码或你正在使用的 crate。让我们尝试通过将 RUST_BACKTRACE 环境变量设置为除 0 以外的任何值来获取回溯。示例 9-2 显示了与你将看到的类似的输出。

The note: line tells us that we can set the RUST_BACKTRACE environment variable to get a backtrace of exactly what happened to cause the error. A backtrace is a list of all the functions that have been called to get to this point. Backtraces in Rust work as they do in other languages: The key to reading the backtrace is to start from the top and read until you see files you wrote. That’s the spot where the problem originated. The lines above that spot are code that your code has called; the lines below are code that called your code. These before-and-after lines might include core Rust code, standard library code, or crates that you’re using. Let’s try to get a backtrace by setting the RUST_BACKTRACE environment variable to any value except 0. Listing 9-2 shows output similar to what you’ll see.

$ 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/librahttps://doc.rust-lang.org/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
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:16:9
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3361:9
   6: panic::main
             at ./src/main.rs:4:6
   7: core::ops::function::FnOnce::call_once
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

输出内容很多!具体的输出可能会根据你的操作系统和 Rust 版本而有所不同。为了获得包含这些信息的回溯,必须启用调试符号。当使用不带 --release 标志的 cargo buildcargo run 时,调试符号是默认启用的,就像我们在这里所做的那样。

That’s a lot of output! The exact output you see might be different depending on your operating system and Rust version. In order to get backtraces with this information, debug symbols must be enabled. Debug symbols are enabled by default when using cargo build or cargo run without the --release flag, as we have here.

在示例 9-2 的输出中,回溯的第 6 行指向了我们项目中导致问题的行:src/main.rs 的第 4 行。如果我们不希望程序发生 panic,我们应该从提到我们编写的文件的第一行所指向的位置开始调查。在示例 9-1 中,我们故意编写了会导致 panic 的代码,修复该 panic 的方法是不要请求超出 vector 索引范围的元素。将来当你的代码发生 panic 时,你需要弄清楚代码正在使用哪些值执行什么操作导致了 panic,以及代码应该改为做什么。

In the output in Listing 9-2, line 6 of the backtrace points to the line in our project that’s causing the problem: line 4 of src/main.rs. If we don’t want our program to panic, we should start our investigation at the location pointed to by the first line mentioning a file we wrote. In Listing 9-1, where we deliberately wrote code that would panic, the way to fix the panic is to not request an element beyond the range of the vector indexes. When your code panics in the future, you’ll need to figure out what action the code is taking with what values to cause the panic and what the code should do instead.

在本章稍后的“要不要 panic!部分,我们将回到 panic! 以及何时应该或不应该使用 panic! 来处理错误情况。接下来,我们将看看如何使用 Result 从错误中恢复。

We’ll come back to panic! and when we should and should not use panic! to handle error conditions in the “To panic! or Not to panic! section later in this chapter. Next, we’ll look at how to recover from an error using Result.