Rust 用 panic! 處理不可恢復(fù)的錯誤

2023-03-22 15:10 更新
ch09-01-unrecoverable-errors-with-panic.md
commit 199ca99926f232ee7f581a917eada4b65ff21754

突然有一天,代碼出問題了,而你對此束手無策。對于這種情況,Rust 有 panic!宏。當執(zhí)行這個宏時,程序會打印出一個錯誤信息,展開并清理棧數(shù)據(jù),然后接著退出。出現(xiàn)這種情況的場景通常是檢測到一些類型的 bug,而且程序員并不清楚該如何處理它。

對應(yīng) panic 時的棧展開或終止

當出現(xiàn) panic 時,程序默認會開始 展開unwinding),這意味著 Rust 會回溯棧并清理它遇到的每一個函數(shù)的數(shù)據(jù),不過這個回溯并清理的過程有很多工作。另一種選擇是直接 終止abort),這會不清理數(shù)據(jù)就退出程序。那么程序所使用的內(nèi)存需要由操作系統(tǒng)來清理。如果你需要項目的最終二進制文件越小越好,panic 時通過在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',可以由展開切換為終止。例如,如果你想要在release模式中 panic 時直接終止:

[profile.release]
panic = 'abort'

讓我們在一個簡單的程序中調(diào)用 panic!

文件名: src/main.rs

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

運行程序?qū)霈F(xiàn)類似這樣的輸出:

$ 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:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

最后兩行包含 panic! 調(diào)用造成的錯誤信息。第一行顯示了 panic 提供的信息并指明了源碼中 panic 出現(xiàn)的位置:src/main.rs:2:5 表明這是 src/main.rs 文件的第二行第五個字符。

在這個例子中,被指明的那一行是我們代碼的一部分,而且查看這一行的話就會發(fā)現(xiàn) panic! 宏的調(diào)用。在其他情況下,panic! 可能會出現(xiàn)在我們的代碼所調(diào)用的代碼中。錯誤信息報告的文件名和行號可能指向別人代碼中的 panic! 宏調(diào)用,而不是我們代碼中最終導(dǎo)致 panic! 的那一行。我們可以使用 panic! 被調(diào)用的函數(shù)的 backtrace 來尋找代碼中出問題的地方。下面我們會詳細介紹 backtrace 是什么。

使用 panic! 的 backtrace

讓我們來看看另一個因為我們代碼中的 bug 引起的別的庫中 panic! 的例子,而不是直接的宏調(diào)用。示例 9-1 有一些嘗試通過索引訪問 vector 中元素的例子:

文件名: src/main.rs

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

運行程序?qū)霈F(xiàn)類似這樣的輸出:

$ 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:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

最后兩行包含 panic! 調(diào)用造成的錯誤信息。第一行顯示了 panic 提供的信息并指明了源碼中 panic 出現(xiàn)的位置:src/main.rs:2:5 表明這是 src/main.rs 文件的第二行第五個字符。

在這個例子中,被指明的那一行是我們代碼的一部分,而且查看這一行的話就會發(fā)現(xiàn) panic! 宏的調(diào)用。在其他情況下,panic! 可能會出現(xiàn)在我們的代碼所調(diào)用的代碼中。錯誤信息報告的文件名和行號可能指向別人代碼中的 panic! 宏調(diào)用,而不是我們代碼中最終導(dǎo)致 panic! 的那一行。我們可以使用 panic! 被調(diào)用的函數(shù)的 backtrace 來尋找代碼中出問題的地方。下面我們會詳細介紹 backtrace 是什么。

使用 panic! 的 backtrace

讓我們來看看另一個因為我們代碼中的 bug 引起的別的庫中 panic! 的例子,而不是直接的宏調(diào)用。示例 9-1 有一些嘗試通過索引訪問 vector 中元素的例子:

文件名: src/main.rs

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

    v[99];
}

示例 9-1:嘗試訪問超越 vector 結(jié)尾的元素,這會造成 panic!

這里嘗試訪問 vector 的第一百個元素(這里的索引是 99 因為索引從 0 開始),不過它只有三個元素。這種情況下 Rust 會 panic。[] 應(yīng)當返回一個元素,不過如果傳遞了一個無效索引,就沒有可供 Rust 返回的正確的元素。

C 語言中,嘗試讀取數(shù)據(jù)結(jié)構(gòu)之后的值是未定義行為(undefined behavior)。你會得到任何對應(yīng)數(shù)據(jù)結(jié)構(gòu)中這個元素的內(nèi)存位置的值,甚至是這些內(nèi)存并不屬于這個數(shù)據(jù)結(jié)構(gòu)的情況。這被稱為 緩沖區(qū)溢出buffer overread),并可能會導(dǎo)致安全漏洞,比如攻擊者可以像這樣操作索引來讀取儲存在數(shù)據(jù)結(jié)構(gòu)之后不被允許的數(shù)據(jù)。

為了保護程序遠離這類漏洞,如果嘗試讀取一個索引不存在的元素,Rust 會停止執(zhí)行并拒絕繼續(xù)。嘗試運行上面的程序會出現(xiàn)如下:

$ 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:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

錯誤指向 main.rs 的第 4 行,這里我們嘗試訪問索引 99。下面的說明(note)行提醒我們可以設(shè)置 RUST_BACKTRACE 環(huán)境變量來得到一個 backtrace。backtrace 是一個執(zhí)行到目前位置所有被調(diào)用的函數(shù)的列表。Rust 的 backtrace 跟其他語言中的一樣:閱讀 backtrace 的關(guān)鍵是從頭開始讀直到發(fā)現(xiàn)你編寫的文件。這就是問題的發(fā)源地。這一行往上是你的代碼所調(diào)用的代碼;往下則是調(diào)用你的代碼的代碼。這些行可能包含核心 Rust 代碼,標準庫代碼或用到的 crate 代碼。讓我們將 RUST_BACKTRACE 環(huán)境變量設(shè)置為任何不是 0 的值來獲取 backtrace 看看。示例 9-2 展示了與你看到類似的輸出:

$ RUST_BACKTRACE=1 cargo run
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/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
   7: core::ops::function::FnOnce::call_once
             at /rustc/7eac88abb2e57e752f3302f02be5f3ce3d7adfb4/library/core/src/ops/function.rs:227
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

示例 9-2:當設(shè)置 RUST_BACKTRACE 環(huán)境變量時 panic! 調(diào)用所生成的 backtrace 信息

這里有大量的輸出!你實際看到的輸出可能因不同的操作系統(tǒng)和 Rust 版本而有所不同。為了獲取帶有這些信息的 backtrace,必須啟用 debug 標識。當不使用 --release 參數(shù)運行 cargo build 或 cargo run 時 debug 標識會默認啟用,就像這里一樣。

示例 9-2 的輸出中,backtrace 的 12 行指向了我們項目中造成問題的行:src/main.rs 的第 4 行。如果你不希望程序 panic,第一個提到我們編寫的代碼行的位置是你應(yīng)該開始調(diào)查的,以便查明是什么值如何在這個地方引起了 panic。在示例 9-1 中,我們故意編寫會 panic 的代碼來演示如何使用 backtrace,修復(fù)這個 panic 的方法就是不要嘗試在一個只包含三個項的 vector 中請求索引是 100 的元素。當將來你的代碼出現(xiàn)了 panic,你需要搞清楚在這特定的場景下代碼中執(zhí)行了什么操作和什么值導(dǎo)致了 panic,以及應(yīng)當如何處理才能避免這個問題。

本章后面的小節(jié) “panic! 還是不 panic!” 會再次回到 panic! 并講解何時應(yīng)該、何時不應(yīng)該使用 panic! 來處理錯誤情況。接下來,我們來看看如何使用 Result 來從錯誤中恢復(fù)。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號