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

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

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

對(duì)應(yīng) panic 時(shí)的棧展開(kāi)或終止

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

[profile.release]
panic = 'abort'

讓我們?cè)谝粋€(gè)簡(jiǎn)單的程序中調(diào)用 panic!

文件名: src/main.rs

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

運(yùn)行程序?qū)?huì)出現(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)用造成的錯(cuò)誤信息。第一行顯示了 panic 提供的信息并指明了源碼中 panic 出現(xiàn)的位置:src/main.rs:2:5 表明這是 src/main.rs 文件的第二行第五個(gè)字符。

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

使用 panic! 的 backtrace

讓我們來(lái)看看另一個(gè)因?yàn)槲覀兇a中的 bug 引起的別的庫(kù)中 panic! 的例子,而不是直接的宏調(diào)用。示例 9-1 有一些嘗試通過(guò)索引訪問(wèn) vector 中元素的例子:

文件名: src/main.rs

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

運(yùn)行程序?qū)?huì)出現(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)用造成的錯(cuò)誤信息。第一行顯示了 panic 提供的信息并指明了源碼中 panic 出現(xiàn)的位置:src/main.rs:2:5 表明這是 src/main.rs 文件的第二行第五個(gè)字符。

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

使用 panic! 的 backtrace

讓我們來(lái)看看另一個(gè)因?yàn)槲覀兇a中的 bug 引起的別的庫(kù)中 panic! 的例子,而不是直接的宏調(diào)用。示例 9-1 有一些嘗試通過(guò)索引訪問(wèn) vector 中元素的例子:

文件名: src/main.rs

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

    v[99];
}

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

這里嘗試訪問(wèn) vector 的第一百個(gè)元素(這里的索引是 99 因?yàn)樗饕龔?0 開(kāi)始),不過(guò)它只有三個(gè)元素。這種情況下 Rust 會(huì) panic。[] 應(yīng)當(dāng)返回一個(gè)元素,不過(guò)如果傳遞了一個(gè)無(wú)效索引,就沒(méi)有可供 Rust 返回的正確的元素。

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

為了保護(hù)程序遠(yuǎn)離這類漏洞,如果嘗試讀取一個(gè)索引不存在的元素,Rust 會(huì)停止執(zhí)行并拒絕繼續(xù)。嘗試運(yùn)行上面的程序會(huì)出現(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

錯(cuò)誤指向 main.rs 的第 4 行,這里我們嘗試訪問(wèn)索引 99。下面的說(shuō)明(note)行提醒我們可以設(shè)置 RUST_BACKTRACE 環(huán)境變量來(lái)得到一個(gè) backtrace。backtrace 是一個(gè)執(zhí)行到目前位置所有被調(diào)用的函數(shù)的列表。Rust 的 backtrace 跟其他語(yǔ)言中的一樣:閱讀 backtrace 的關(guān)鍵是從頭開(kāi)始讀直到發(fā)現(xiàn)你編寫的文件。這就是問(wèn)題的發(fā)源地。這一行往上是你的代碼所調(diào)用的代碼;往下則是調(diào)用你的代碼的代碼。這些行可能包含核心 Rust 代碼,標(biāo)準(zhǔn)庫(kù)代碼或用到的 crate 代碼。讓我們將 RUST_BACKTRACE 環(huán)境變量設(shè)置為任何不是 0 的值來(lái)獲取 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:當(dāng)設(shè)置 RUST_BACKTRACE 環(huán)境變量時(shí) panic! 調(diào)用所生成的 backtrace 信息

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

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

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


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)