無法恢復的錯誤與 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! 來處理錯誤情況。