Bagaimana panik bekerja di Rust

Cara Kerja Panic Rust


Apa yang sebenarnya terjadi ketika Anda memanggil panic!() ?
Baru-baru ini, saya menghabiskan banyak waktu mempelajari bagian-bagian dari perpustakaan standar yang terkait dengan ini dan ternyata jawabannya agak rumit!


Saya tidak dapat menemukan dokumen yang menjelaskan gambaran umum kepanikan di Rust, jadi ada baiknya menuliskannya.


(Artikel tak tahu malu: Alasan saya menjadi tertarik pada topik ini adalah bahwa @Aaron1011 mengimplementasikan dukungan untuk stack unwinding di Miri.


Saya ingin melihat ini di Miri sejak dahulu kala, dan saya tidak pernah punya waktu untuk mengimplementasikannya sendiri, jadi sungguh luar biasa melihat bagaimana seseorang mengirim PR untuk mendukung ini secara tiba-tiba.


Setelah banyak putaran memeriksa kode, kode tersebut telah disuntikkan baru-baru ini.


Masih ada beberapa sisi yang kasar , tetapi dasar-dasarnya didefinisikan dengan baik.)


Tujuan artikel ini adalah untuk mendokumentasikan struktur tingkat tinggi dan antarmuka terkait yang ikut bermain di sisi Rust.


Mekanisme stack unwinding yang sebenarnya adalah masalah yang sama sekali berbeda (di mana saya tidak berwenang untuk berbicara).


Catatan: artikel ini menjelaskan tentang kepanikan dari komit ini .


Banyak antarmuka yang dijelaskan di sini adalah internal libstd yang tidak stabil dan dapat berubah kapan saja.


Struktur tingkat tinggi


Mencoba memahami cara kerja panik saat membaca kode di libstd, Anda dapat dengan mudah tersesat di labirin.
Ada beberapa tingkat tipuan yang hanya terhubung oleh tautan,
ada #[panic_handler] dan "runtime panic handler" (dikontrol oleh strategi panik yang diatur melalui -C panic ) dan "panic traps" , dan ternyata kepanikan itu dalam konteks #[no_std] memerlukan jalur kode yang sangat berbeda ... sangat banyak yang terjadi.


Lebih buruk lagi, RFC menggambarkan perangkap panik menyebut mereka "pengendali panik," tetapi istilah itu telah didefinisikan ulang.


Saya pikir tempat terbaik untuk memulai adalah dengan antarmuka yang mengontrol dua arah:


  • Panic pengendali runtime digunakan oleh libstd untuk mengontrol apa yang terjadi setelah informasi panik dicetak ke stderr.
    Ini ditentukan oleh strategi panik: apakah kita menginterupsi ( -C panic=abort ) atau memulai pelonggaran tumpukan ( -C panic=unwind ).
    (Menangani panik run-time juga menyediakan implementasi untuk catch_unwind , tetapi kami tidak akan membicarakannya di sini.)


  • Panic handler digunakan oleh libcore untuk mengimplementasikan (a) kepanikan yang dimasukkan oleh pembuatan kode (seperti panik yang disebabkan oleh aritmatika melimpah atau pengindeksan array / slice di luar batas) dan (b) core::panic! makro (ini adalah panic! makro di libcore itu sendiri dan dalam #[no_std] konteks #[no_std] ).



Kedua antarmuka ini diimplementasikan melalui blok extern : listd / libcore, masing-masing, cukup mengimpor beberapa fungsi yang didelegasikan, dan di tempat lain di pohon peti fungsi ini diimplementasikan.


Impor hanya diizinkan selama pengikatan; Melihat secara lokal pada kode, seseorang tidak bisa mengatakan di mana implementasi aktual dari antarmuka yang sesuai hidup.
Tidak mengherankan bahwa saya tersesat beberapa kali di sepanjang jalan.


Di masa depan, kedua antarmuka ini akan sangat berguna; ketika kamu mengacau. Hal pertama yang harus diperiksa adalah apakah Anda bingung dengan panic handler dan run-time panic handler.
(Dan ingat bahwa ada juga pencegat panik, kita akan sampai ke mereka.)
Itu terjadi pada saya sepanjang waktu.


Apalagi core::panic! dan std::panic! tidak sama; seperti yang akan kita lihat, mereka menggunakan jalur kode yang sangat berbeda.


libcore dan libstd masing-masing menerapkan cara mereka sendiri untuk menyebabkan kepanikan:


  • core::panic! libcore sangat kecil: itu hanya langsung mendelegasikan kepanikan kepada pawang .


  • libstd std::panic! ( panic! "normal" panic! in Rust) meluncurkan mesin panik yang berfungsi penuh yang menyediakan intersepsi panik yang dikendalikan pengguna.
    Hook default akan menampilkan pesan panik di stderr.
    Setelah fungsi intersepsi selesai, libstd mendelegasikannya ke run-time panic handler.


    libstd juga menyediakan pengendali panik yang memanggil mekanisme yang sama, jadi core::panic! juga berakhir di sini.



Mari kita lihat bagian-bagian ini lebih detail.


Menangani kepanikan selama eksekusi program


Antarmuka untuk runtime panik (diwakili oleh RFC ini ) adalah fungsi __rust_start_panic(payload: usize) -> u32 yang diimpor oleh libstd dan kemudian diselesaikan oleh tautan.


Argumen usize sini sebenarnya *mut &mut dyn core::panic::BoxMeUp - ini adalah tempat di mana *mut &mut dyn core::panic::BoxMeUp "data yang berguna" dari panik (informasi tersedia saat terdeteksi).


BoxMeUp adalah detail implementasi internal yang tidak stabil, tetapi melihat jenis ini, kita melihat bahwa yang dilakukannya hanyalah membungkus dyn Any + Send , yang merupakan tipe data panik yang berguna yang dikembalikan oleh catch_unwind dan thread::spawn .


BoxMeUp::box_me_up mengembalikan Box<dyn Any + Send> , tetapi sebagai pointer mentah (karena Box tidak tersedia dalam konteks di mana tipe ini didefinisikan); BoxMeUp::get pinjam konten saja.


Dua implementasi dari antarmuka ini disediakan di libpanic_unwind : libpanic_unwind untuk -C panic=unwind (default pada kebanyakan platform) dan libpanic_abort untuk -C panic=abort .


std::panic!


Di atas antarmuka runtime panik, libstd mengimplementasikan mekanisme karat panik default di modul internal std::panicking .


rust_panic_with_hook


Fungsi kunci di mana hampir semuanya berjalan adalah rust_panic_with_hook :


 fn rust_panic_with_hook( payload: &mut dyn BoxMeUp, message: Option<&fmt::Arguments<'_>>, file_line_col: &(&str, u32, u32), ) -> ! 

Fungsi ini menerima lokasi sumber panik, pesan opsional yang tidak diformat (lihat fmt::Arguments Dokumentasi fmt::Arguments ), dan data yang berguna.


Tugas utamanya adalah untuk memicu apa yang menjadi pencegat panik saat ini.
Pencegat panik memiliki argumen PanicInfo , jadi kami membutuhkan lokasi sumber panik, informasi format untuk pesan panik, dan data yang berguna.
Ini cocok dengan argumen rust_panic_with_hook sangat baik!
file_line_col dan message dapat digunakan secara langsung untuk dua elemen pertama; payload berubah menjadi &(dyn Any + Send) melalui antarmuka BoxMeUp .


Menariknya, pencegat panik standar sepenuhnya mengabaikan message ; apa yang Anda lihat adalah membuang payload ke &str atau String (apa pun yang berhasil).
Agaknya, penelepon harus memastikan bahwa pemformatan message , jika ada, menghasilkan hasil yang sama.
(Dan yang kita bahas di bawah ini menjamin ini.)


Akhirnya, rust_panic_with_hook dikirim ke pengendali panik runtime saat ini.


Saat ini, hanya payload yang masih relevan - dan yang penting: message (dengan masa pakai '_ menunjukkan bahwa tautan yang berumur pendek mungkin terkandung, tetapi data panik yang berguna akan menyebar ke tumpukan dan karenanya harus dengan masa pakai 'static ).


'static Kendala 'static sana cukup tersembunyi, tetapi setelah beberapa saat saya menyadari bahwa Any means 'static (dan ingat bahwa dyn BoxMeUp hanya digunakan untuk mendapatkan Box<dyn Any + Send> ).


Titik masuk Libstd


rust_panic_with_hook adalah fungsi pribadi untuk std::panicking ; modul ini menyediakan tiga titik masuk di atas fungsi pusat ini dan satu yang memintasnya:


  • Implementasi panic handler default yang mendukung (seperti yang akan kita lihat) panic from core::panic! dan built-in panic (dari arithmetic overflow atau array / slice indexing).
    Mendapat PanicInfo sebagai input, dan harus mengubahnya menjadi argumen untuk rust_panic_with_hook .
    Anehnya, meskipun komponen PanicInfo dan argumen rust_panic_with_hook sangat mirip, dan tampaknya mereka hanya dapat diteruskan, tidak .
    Sebaliknya, libstd sepenuhnya mengabaikan komponen payload dari PanicInfo dan menetapkan payload aktual (diteruskan ke rust_panic_with_hook ) sehingga berisi message .


    Secara khusus, ini berarti bahwa run- time panic handler tidak masalah untuk aplikasi no_std .
    Ini hanya berlaku ketika implementasi dari panic handler di libstd digunakan.
    ( Strategi panik dipilih melalui -C panic masih penting, karena juga mempengaruhi pembuatan kode.
    Misalnya, dengan -C panic=abort kode mungkin menjadi lebih sederhana, karena Anda tidak perlu mendukung stack unwinding).


  • begin_panic_fmt , mendukung versi std::panic! (mis. ini digunakan ketika Anda meneruskan beberapa argumen ke makro).
    Pada dasarnya, hanya membungkus argumen format string di PanicInfo (dengan muatan tiruan ) dan memanggil panic handler default yang baru saja kita bahas, terjadi.


  • begin_panic mendukung std::panic! dengan std::panic! .
    Menariknya, ini menggunakan jalur kode yang sama sekali berbeda dari pada dua titik masuk lainnya!
    Secara khusus, ini adalah satu-satunya titik masuk yang memungkinkan Anda untuk mentransfer data berguna yang sewenang-wenang .
    Payload ini hanya Box<dyn Any + Send> sehingga dapat diteruskan ke rust_panic_with_hook , dan hanya itu.


    Secara khusus, pencegat panik yang melihat bidang message dari PanicData tidak akan dapat melihat pesan di std::panic!("do panic") , tetapi dapat melihat pesan di std::panic!("panic with data: {}", data) karena yang terakhir melewati begin_panic_fmt .
    Itu tampak sangat mengagumkan. (Tetapi juga perhatikan bahwa PanicData::message() belum stabil.)


  • update_count_then_panic ternyata aneh: titik entri ini mendukung resume_unwind dan tidak benar - benar menyebabkan intersepsi panik.
    Sebagai gantinya, itu segera dikirim ke pengendali panik.
    Sebagai contoh, begin_panic memungkinkan penelepon untuk memilih data berguna yang sewenang-wenang.
    Tidak seperti begin_panic , fungsi panggilan bertanggung jawab untuk mengemas dan mengukur muatan; Fungsi update_count_then_panic hanya meneruskan argumennya hampir kata demi kata ke pengendali panik run-time.



Penangan panik


std::panic! Mekanisme ini sangat berguna, tetapi membutuhkan penempatan data di heap melalui Box , yang tidak selalu tersedia.
Untuk memberikan libcore cara untuk menyebabkan panik, penangan panik diperkenalkan.
Seperti yang telah kita lihat, jika libstd tersedia, ia menyediakan implementasi core::panic! antarmuka ini core::panic! panik dalam tampilan libstd.


Antarmuka untuk pengendali panik adalah fungsi fn panic(info: &core::panic::PanicInfo) -> ! Libcore diimpor, dan ini kemudian diselesaikan oleh linker.
Jenis PanicInfo sama dengan untuk pencegat panik: berisi lokasi sumber panik, pesan panik, dan data yang berguna ( dyn Any + Send ).
Pesan panik disajikan dalam bentuk fmt::Arguments , yaitu string format dengan argumen yang belum diformat.


core::panic!


Selain antarmuka prosesor panik, libcore menyediakan API panik minimal .
core::panic! makro menciptakan fmt::Arguments yang kemudian diteruskan ke pengendali panik .
Pemformatan tidak terjadi di sini, karena ini akan membutuhkan alokasi memori pada heap; Inilah sebabnya mengapa PanicInfo berisi string format " PanicInfo " dengan argumennya.


Anehnya, bidang payload dari PanicInfo diteruskan ke panic handler, selalu disetel ke nilai dummy .
Ini menjelaskan mengapa panic handler libstd mengabaikan data payload (dan sebaliknya membuat data payload baru dari message ), tapi itu membuat saya bertanya-tanya mengapa bidang ini adalah bagian dari API panic handler.
Konsekuensi lain dari ini adalah bahwa core::panic!("message") dan std::panic!("message") (opsi tanpa pemformatan) benar-benar menyebabkan panik yang sangat berbeda: yang pertama berubah menjadi fmt::Arguments , melewati antarmuka panic handler, dan kemudian libstd membuat data String berguna dengan memformatnya.
Yang terakhir, bagaimanapun, secara langsung menggunakan &str sebagai data yang berguna, dan bidang message tetap None (sebagaimana telah disebutkan).


Beberapa elemen panic API dalam libcore adalah elemen bahasa karena kompiler memasukkan panggilan ke fungsi-fungsi ini selama pembuatan kode:


  • Elemen panic dipanggil ketika kompiler perlu menyebabkan kepanikan yang tidak memerlukan format apa pun (misalnya, aritmatika melimpah); ini adalah fungsi yang sama yang juga mendukung core::panic! dengan satu argumen core::panic! .
  • panic_bounds_check dipanggil ketika pemeriksaan batas array / slice gagal, ia memanggil metode yang sama dengan core::panic! dengan format .

Kesimpulan


Kami melewati 4 level API, 2 di antaranya dialihkan melalui panggilan fungsi yang diimpor dan diselesaikan oleh tautan.
Perjalanan yang luar biasa!
Tetapi kita telah mencapai akhir.
Saya harap Anda tidak panik di sepanjang jalan. ;)


Saya menyebutkan beberapa hal luar biasa.
Ternyata mereka semua terhubung dengan fakta bahwa pencegat panik dan prosesor panik berbagi struktur PanicInfo di antarmuka mereka, yang berisi message dan payload diformat secara opsional dengan jenis yang dihapus:


  • Pencegat panik selalu dapat menemukan pesan yang sudah diformat dalam payload , sehingga message tampaknya tidak ada gunanya bagi pencegat. Bahkan, message mungkin tidak ada bahkan jika payload berisi pesan (misalnya, untuk std::panic!("message") ).
  • Panic handler tidak akan pernah benar-benar menerima payload , sehingga bidang tersebut tampaknya tidak ada gunanya bagi handler.

Membaca RFC dari deskripsi panic handler , sepertinya rencananya adalah untuk core::panic! juga mendukung data bermanfaat yang sewenang-wenang, tetapi sejauh ini belum terwujud.
Namun demikian, bahkan dengan ekstensi masa depan ini, saya pikir kami memiliki invarian bahwa ketika message adalah Some , maka payload == &NoPayload (oleh karena itu data yang berguna redundan) atau payload adalah pesan yang diformat (karena itu pesannya redundan).


Saya bertanya-tanya apakah ada kasus di mana kedua bidang akan berguna dan jika tidak, dapatkah kita menyandikannya dengan menjadikannya dua varian enum ?


Mungkin ada alasan bagus terhadap proposal ini untuk desain saat ini; akan sangat bagus untuk mendapatkannya di suatu tempat dalam format dokumentasi. :)


Ada banyak lagi yang bisa dikatakan, tetapi pada titik ini saya mengundang Anda untuk mengikuti tautan ke kode sumber yang saya sertakan di atas.


Dengan mengingat struktur tingkat tinggi, Anda harus dapat mengikuti kode ini.
Jika orang berpikir bahwa ulasan ini harus ditempatkan di suatu tempat selamanya, saya akan senang mengubah artikel ini menjadi blog menjadi semacam dokumentasi - walaupun saya tidak yakin apakah ini akan menjadi tempat yang baik.


Dan jika Anda menemukan kesalahan dalam apa yang saya tulis, beri tahu saya!

Source: https://habr.com/ru/post/id482358/


All Articles