Pengecualian khusus dalam .NET dan cara menyiapkannya

Berbagai pengecualian di .NET memiliki karakteristiknya sendiri, dan dapat sangat berguna untuk mengetahuinya. Bagaimana cara menipu CLR? Bagaimana agar tetap hidup di runtime dengan menangkap StackOverflowException? Pengecualian apa yang tampaknya tidak mungkin ditangkap, tetapi jika Anda benar-benar menginginkannya, bukan?



Di bawah potongan, transkrip laporan Eugene ( epeshk ) Peshkov dari konferensi Piter DotNext 2018 kami, di mana ia membicarakan hal ini dan fitur pengecualian lainnya.



Hai Nama saya Eugene. Saya bekerja untuk SKB Kontur dan mengembangkan sistem hosting dan menyebarkan aplikasi untuk Windows. Intinya adalah bahwa kami memiliki banyak tim produk yang menulis layanan mereka sendiri dan menjamu mereka bersama kami. Kami memberi mereka solusi mudah dan sederhana untuk berbagai tugas infrastruktur. Misalnya, untuk memantau konsumsi sumber daya sistem atau menyelesaikan replika ke layanan.

Terkadang ternyata aplikasi yang dihosting di sistem kami berantakan. Kami telah melihat banyak cara bagaimana aplikasi dapat crash di runtime. Salah satu metode tersebut adalah membuang beberapa pengecualian yang tak terduga dan mempesona.

Hari ini saya akan berbicara tentang fitur pengecualian di .NET. Kami menemui beberapa fitur ini dalam produksi, dan beberapa di antaranya dalam proses percobaan.

Rencanakan


  1. .NET Exception Behavior
  2. Penanganan pengecualian Windows dan peretasan

Semua hal berikut ini berlaku untuk Windows. Semua contoh diuji pada versi terbaru dari kerangka penuh .NET 4.7.1. Juga akan ada beberapa referensi untuk .NET Core.

Pelanggaran akses


Pengecualian ini terjadi selama operasi memori yang salah. Misalnya, jika suatu aplikasi mencoba mengakses area memori yang tidak dapat diaksesnya. Pengecualiannya adalah level rendah, dan biasanya, jika itu terjadi, diperlukan debugging yang sangat lama.

Mari kita coba dapatkan pengecualian ini menggunakan C #. Untuk melakukan ini, kami menulis byte 42 ke alamat 1000 (kami menganggap bahwa 1000 adalah alamat yang cukup acak dan aplikasi kami kemungkinan besar tidak memiliki akses ke sana).

try { Marshal.WriteByte((IntPtr) 1000, 42); } catch (AccessViolationException) { ... } 

WriteByte melakukan apa yang kita butuhkan: ia menulis byte ke alamat yang diberikan. Kami berharap panggilan ini untuk melemparkan AccessViolationException. Kode ini memang akan membuang pengecualian ini, itu akan dapat menangani dan aplikasi akan terus bekerja. Sekarang mari kita ubah sedikit kode:

 try { var bytes = new byte[] {42}; Marshal.Copy(bytes, 0, (IntPtr) 1000, bytes.Length); } catch (AccessViolationException) { ... } 

Jika alih-alih WriteByte Anda menggunakan metode Salin dan salin byte 42 ke alamat 1000, kemudian menggunakan try-catch, AccessViolation tidak dapat ditangkap. Pada saat yang sama, sebuah pesan akan ditampilkan pada konsol yang menyatakan bahwa aplikasi dihentikan karena AccessViolationException yang tidak ditangani.

 Marshal.Copy(bytes, 0, (IntPtr) 1000, bytes.Length); Marshal.WriteByte((IntPtr) 1000, 42); 

Ternyata kami memiliki dua baris kode, sedangkan yang pertama crash seluruh aplikasi dengan AccessViolation, dan yang kedua melempar pengecualian yang diproses dari jenis yang sama. Untuk memahami mengapa ini terjadi, kita akan melihat bagaimana metode ini diatur dari dalam.

Mari kita mulai dengan metode Salin.

 static void Copy(...) { Marshal.CopyToNative((object) source, startIndex, destination, length); } [MethodImpl(MethodImplOptions.InternalCall)] static extern void CopyToNative(object source, int startIndex, IntPtr destination, int length); 

Satu-satunya hal yang dilakukan metode Salin adalah memanggil metode CopyToNative, diimplementasikan dalam. Jika aplikasi kita masih macet dan pengecualian terjadi di suatu tempat, maka ini hanya dapat terjadi di dalam CopyToNative. Dari sini kita dapat melakukan pengamatan pertama: jika kode .NET disebut kode asli dan AccessViolation terjadi di dalamnya, maka kode .NET tidak dapat menangani pengecualian ini karena beberapa alasan.

Sekarang kita akan mengerti mengapa dimungkinkan untuk memproses AccessViolation menggunakan metode WriteByte. Mari kita lihat kode untuk metode ini:

 unsafe static void WriteByte(IntPtr ptr, byte val) { try { *(byte*) ptr = val; } catch (NullReferenceException) {     // this method is documented to throw AccessViolationException on any AV throw new AccessViolationException(); } } 

Metode ini sepenuhnya diterapkan dalam kode terkelola. Menggunakan C # -pointer untuk menulis data ke alamat yang diinginkan, dan juga menangkap NullReferenceException. Jika NRE dicegat, AccessViolationException dilempar. Jadi itu perlu karena spesifikasinya . Dalam hal ini, semua pengecualian yang dilemparkan oleh konstruk lemparan ditangani. Dengan demikian, jika NullReferenceException terjadi selama eksekusi kode di dalam WriteByte, kita bisa menangkap AccessViolation. Bisakah NRE terjadi, dalam kasus kami, ketika mengakses alamat 1000 daripada alamat nol?

Kami menulis ulang kode menggunakan pointer C # secara langsung, dan melihat bahwa ketika mengakses alamat yang tidak nol, NullReferenceException sebenarnya dilemparkan:

 *(byte*) 1000 = 42; 

Untuk memahami mengapa ini terjadi, kita perlu mengingat bagaimana memori proses bekerja. Dalam memori proses, semua alamat adalah virtual. Ini berarti bahwa aplikasi memiliki ruang alamat yang besar dan hanya beberapa halaman darinya yang ditampilkan dalam memori fisik nyata. Tetapi ada kekhasan: alamat 64 KB pertama tidak pernah dipetakan ke memori fisik dan tidak diberikan ke aplikasi. Rantime .NET tahu ini dan menggunakannya. Jika AccessViolation terjadi dalam kode yang dikelola, maka runtime akan memeriksa alamat mana dalam memori yang diakses dan menghasilkan pengecualian yang sesuai. Untuk alamat dari 0 hingga 2 ^ 16 - NullReference, untuk semua yang lain - AccessViolation.



Mari kita lihat mengapa NullReference terlempar tidak hanya saat mengakses ke alamat nol. Bayangkan Anda sedang mengakses bidang objek jenis referensi, dan referensi ke objek ini adalah nol:



Dalam situasi ini, kami berharap mendapatkan NullReferenceException. Akses ke bidang objek terjadi dengan mengimbangi relatif ke alamat objek ini. Ternyata kami akan beralih ke alamat yang cukup dekat ke nol (ingat bahwa tautan ke objek asli kami adalah nol). Dengan perilaku runtime ini, kami mendapatkan pengecualian yang diharapkan tanpa verifikasi tambahan dari alamat objek itu sendiri.

Tetapi apa yang terjadi jika kita beralih ke bidang objek, dan objek ini sendiri membutuhkan lebih dari 64 KB?



Bisakah kita mendapatkan AccessViolation dalam kasus ini? Ayo lakukan percobaan. Mari kita buat objek yang sangat besar dan kita akan merujuk ke bidangnya. Satu bidang di awal objek, yang kedua di akhir:



Kedua metode akan melempar NullReferenceException. AccessViolationException tidak akan terjadi.
Mari kita lihat instruksi yang akan dihasilkan untuk metode ini. Dalam kasus kedua, kompiler JIT menambahkan instruksi cmp tambahan yang mengakses alamat objek itu sendiri, sehingga memanggil AccessViolation dengan alamat nol, yang akan dikonversi oleh runtime ke NullReferenceException.

Perlu dicatat bahwa untuk percobaan ini tidak cukup menggunakan array sebagai objek besar. Mengapa Serahkan pertanyaan ini kepada pembaca, tulis ide di komentar :)

Mari kita simpulkan eksperimen dengan AccessViolation.



AccessViolationException berperilaku berbeda tergantung di mana pengecualian terjadi (dalam kode terkelola atau asli). Selain itu, jika pengecualian terjadi pada kode yang dikelola, alamat objek akan diperiksa.

Pertanyaannya adalah: dapatkah kita menangani AccessViolationException yang terjadi dalam kode asli atau dalam kode terkelola, tetapi tidak dikonversi ke NullReference dan tidak dibuang menggunakan lemparan? Ini kadang-kadang fitur yang berguna, terutama ketika bekerja dengan kode yang tidak aman. Jawaban untuk pertanyaan ini tergantung pada versi .NET.



Di .NET 1.0, sama sekali tidak ada AccessViolationException. Semua tautan dianggap valid atau null. Pada saat .NET 2.0, menjadi jelas bahwa tanpa kerja langsung dengan memori - tidak mungkin, dan AccessViolation muncul, sementara itu dapat diproses. Di 4.0 dan di atas, itu masih bisa diterapkan, tetapi memprosesnya tidak begitu sederhana. Untuk menangkap pengecualian ini, Anda sekarang harus menandai metode di mana blok tangkap terletak dengan atribut HandleProcessCorruptedStateException. Rupanya, para pengembang melakukan ini karena mereka berpikir bahwa AccessViolationException bukan pengecualian yang harus ditangkap dalam aplikasi reguler.
Selain itu, untuk kompatibilitas ke belakang, dimungkinkan untuk menggunakan pengaturan runtime:

  • legacyNullReferenceExceptionPolicy mengembalikan perilaku .NET 1.0 - semua AV berubah menjadi NRE
  • legacyCorruptedStateExceptionsPolicy mengembalikan perilaku .NET 2.0 - semua AV dicegat

Di .NET, Core AccessViolation tidak ditangani sama sekali.

Dalam produksi kami ada situasi seperti itu:



Aplikasi yang dibangun di bawah .NET 4.7.1 menggunakan pustaka kode bersama yang dibangun di bawah .NET 3.5. Ada pembantu di perpustakaan ini untuk menjalankan tindakan berkala:

 while (isRunning) { try { action(); } catch (Exception e) { log.Error(e); } WaitForNextExecution(... ); } 

Kami meneruskan tindakan dari aplikasi kami ke penolong ini. Kebetulan dia jatuh dengan AccessViolation. Akibatnya, aplikasi kami terus-menerus mencatat AccessViolation, alih-alih mogok karena kode di perpustakaan di bawah 3,5 bisa menangkapnya. Perlu dicatat bahwa intersepsi tidak tergantung pada versi runtime tempat aplikasi berjalan, tetapi pada TargetFramework, di mana aplikasi itu dibangun, dan dependensinya.

Untuk meringkas. Pemrosesan AccessVilolation tergantung pada asalnya - dalam kode asli atau dikelola - serta pengaturan TargetFramework dan runtime.

Batalkan utas


Terkadang dalam kode Anda harus menghentikan eksekusi salah satu utas. Untuk melakukan ini, Anda dapat menggunakan utas. Amort ();

 var thread = new Thread(() => { try { ... } catch (ThreadAbortException e) { ... Thread.ResetAbort(); } }); ... thread.Abort(); 

Ketika metode Abort dipanggil dalam thread yang dihentikan, sebuah ThreadAbortException dilemparkan. Mari kita menganalisis fitur-fiturnya. Misalnya, kode seperti ini:

 var thread = new Thread(() => { try { … } catch (ThreadAbortException e) { … } }); ... thread.Abort(); 

Sepadan dengan ini:

 var thread = new Thread(() => { try { ... } catch (ThreadAbortException e) { ... throw; } }); ... thread.Abort(); 

Jika Anda masih perlu memproses ThreadAbort dan melakukan beberapa tindakan lain di utas terhenti, maka Anda dapat menggunakan metode Thread.ResetAbort (); Ini menghentikan proses menghentikan aliran dan pengecualian berhenti melemparkan lebih tinggi ke tumpukan. Penting untuk memahami bahwa metode thread.Abort () itu sendiri tidak menjamin apa pun - kode di utas terhenti dapat mencegahnya berhenti.

Fitur lain dari thread.Abort () adalah bahwa ia tidak akan dapat mengganggu kode jika ada dalam tangkapan dan akhirnya blok.

Di dalam kode kerangka kerja, Anda sering dapat menemukan metode di mana blok coba kosong dan semua logika akhirnya ada di dalam. Ini dilakukan hanya untuk mencegah kode ini dilempar oleh ThreadAbortException.

Juga, panggilan ke metode thread.Abort () menunggu untuk ThreadAbortException untuk melemparkan. Gabungkan kedua fakta ini dan dapatkan bahwa metode thread.Abort () dapat memblokir utas panggilan.

 var thread = new Thread(() => {   try { }       catch { } // <-- No ThreadAbortException in catch       finally { // <-- No ThreadAbortException in finally           Thread.Sleep(- 1); } }); thread.Start(); ... thread.Abort(); // Never returns 

Pada kenyataannya, ini bisa ditemui ketika menggunakan menggunakan. Itu digunakan di try / akhirnya, di dalam akhirnya, metode Buang disebut. Ini bisa kompleks semena-mena, berisi penangan acara, gunakan kunci. Dan jika thread.Abort dipanggil saat dijalankan, Buang - thread.Abort () akan menunggu untuk itu. Jadi kami mendapatkan kunci hampir dari awal.

Dalam .NET Core, metode thread.Abort () melempar PlatformNotSupportedException. Dan saya pikir ini sangat bagus, karena itu memotivasi saya untuk menggunakan bukan thread.Abort (), tetapi metode non-invasif untuk menghentikan eksekusi kode, misalnya menggunakan CancelledToken.

LUAR MEMORI


Pengecualian ini dapat diperoleh jika memori pada mesin kurang dari yang dibutuhkan. Atau ketika kita mengalami keterbatasan proses 32-bit. Tetapi Anda bisa mendapatkannya bahkan jika komputer memiliki banyak memori bebas, dan prosesnya 64-bit.

 var arr4gb = new int[int.MaxValue/2]; 

Kode di atas akan membuang OutOfMemory. Masalahnya adalah, secara default, objek yang lebih besar dari 2 GB tidak diperbolehkan. Ini dapat diperbaiki dengan mengatur gcAllowVeryLargeObjects di App.config. Dalam hal ini, array 4 GB dibuat.

Sekarang mari kita coba membuat array lebih banyak lagi.

 var largeArr = new int[int.MaxValue]; 

Sekarang bahkan gcAllowVeryLargeObjects tidak akan membantu. Ini karena .NET memiliki batasan pada indeks maksimum dalam array . Pembatasan ini kurang dari int.MaxValue.

Indeks array maks:

  • array byte - 0x7FFFFFC7
  • array lainnya - 0X7F E FFFFF

Dalam hal ini, OutOfMemoryException akan terjadi, meskipun pada kenyataannya kita telah menghadapi pembatasan tipe data, bukan kekurangan memori.

Terkadang OutOfMemory secara eksplisit dibuang oleh kode terkelola di dalam kerangka .NET:


Ini adalah implementasi dari metode string.Compat. Jika panjang string hasil lebih besar dari int.MaxValue, OutOfMemoryException segera dibuang.

Mari kita beralih ke situasi ketika OutOfMemory muncul dalam kasus ketika memori benar-benar habis.

 LimitMemory(64.Mb()); try { while (true)   list.Add(new byte[size]); } catch (OutOfMemoryException e) { Console.WriteLine(e); } 

Pertama, kami membatasi memori proses kami menjadi 64 MB. Selanjutnya, di dalam loop, pilih array byte baru, simpan ke beberapa lembar sehingga GC tidak mengumpulkannya, dan cobalah untuk menangkap OutOfMemory.

Dalam hal ini, apa pun bisa terjadi:

  • Pengecualian ditangani
  • Proses akan jatuh
  • Mari kita menangkap, tetapi pengecualian akan crash lagi
  • Mari kita mulai menangkap, tetapi StackOverflow akan crash

Dalam hal ini, program akan sepenuhnya non-deterministik. Mari kita menganalisis semua opsi:

  1. Pengecualian dapat ditangani. Di dalam .NET, tidak ada yang menghentikan Anda dari menangani OutOfMemoryException.
  2. Prosesnya mungkin jatuh. Jangan lupa bahwa kami memiliki aplikasi yang dikelola. Ini berarti bahwa di dalamnya dieksekusi tidak hanya kode kita, tetapi juga kode runtime. Misalnya, GC. Dengan demikian, suatu situasi dapat terjadi ketika runtime ingin mengalokasikan memori untuk dirinya sendiri, tetapi tidak dapat melakukannya, maka kita tidak akan dapat menangkap pengecualian.
  3. Mari kita masuk ke tangkapan, tetapi pengecualian akan crash lagi. Di dalam tangkapan, kami juga melakukan pekerjaan di mana kami membutuhkan memori (kami mencetak pengecualian untuk konsol), dan ini dapat menyebabkan pengecualian baru.
  4. Mari kita mulai menangkap, tetapi StackOverflow akan crash. StackOverflow sendiri terjadi ketika metode WriteLine dipanggil, tetapi tidak ada stack overflow di sini, tetapi situasi yang berbeda terjadi. Mari kita analisa lebih detail.



Dalam memori virtual, halaman tidak hanya dapat dipetakan ke memori fisik, tetapi juga dapat dipesan. Jika halaman dicadangkan, maka aplikasi mencatat bahwa itu akan menggunakannya. Jika halaman sudah dipetakan ke memori nyata atau swap, maka itu disebut "berkomitmen" (berkomitmen). Tumpukan menggunakan kemampuan ini untuk membagi memori menjadi cadangan dan berkomitmen. Itu terlihat seperti ini:



Ternyata kita memanggil metode WriteLine, yang mengambil beberapa tempat di stack. Ternyata semua memori yang dikunci telah berakhir, yang berarti sistem operasi saat ini harus mengambil halaman cadangan lain di tumpukan dan memetakannya ke memori fisik nyata, yang sudah diisi dengan byte array. Ini mengarah ke pengecualian StackOverflow.

Kode berikut akan memungkinkan Anda untuk mengkomit semua memori ke stack pada awal aliran sekaligus.

 new Thread(() => F(), 4*1024*1024).Start(); 

Atau, Anda dapat menggunakan pengaturan runtime disableCommitThreadStack. Itu perlu dinonaktifkan agar tumpukan thread dilakukan sebelumnya. Perlu dicatat bahwa perilaku default yang dijelaskan dalam dokumentasi dan diamati pada kenyataannya berbeda.



Stack overflow


Mari kita lihat lebih dekat StackOverflowException. Mari kita lihat dua contoh kode. Di salah satu dari mereka, kita menjalankan rekursi tak terbatas, yang mengarah ke stack overflow, yang kedua kita hanya melempar pengecualian ini dengan throw.

 try { InfiniteRecursion(); } catch (Exception) { ... } 

 try { throw new StackOverflowException(); } catch (Exception) { ... } 

Karena semua pengecualian yang dilemparkan dengan lemparan ditangani, dalam kasus kedua kita akan menangkap pengecualian. Dan dengan kasus pertama, semuanya lebih menarik. Beralih ke MSDN :

"Anda tidak dapat menangkap pengecualian stack overflow, karena kode penanganan pengecualian mungkin memerlukan stack."
MSDN

Dikatakan di sini bahwa kita tidak akan dapat menangkap StackOverflowException, karena intersepsi itu sendiri mungkin memerlukan ruang stack tambahan yang telah berakhir.

Untuk melindungi dari pengecualian ini, kita dapat melakukan hal berikut. Pertama, Anda dapat membatasi kedalaman rekursi. Kedua, Anda bisa menggunakan metode dari kelas RuntimeHelpers:

RuntimeHelpers.EnsureSufficientExecutionStack ();

  • "Memastikan bahwa ruang tumpukan yang tersisa cukup besar untuk menjalankan fungsi .NET Framework." - MSDN
  • InsufficientExecutionStackException
  • 512 KB - x86, AnyCPU, 2 MB - x64 (setengah dari ukuran tumpukan)
  • 64/128 KB - .NET Core
  • Periksa hanya ruang alamat tumpukan


Dokumentasi untuk metode ini mengatakan bahwa ia memeriksa bahwa ada cukup ruang pada stack untuk menjalankan fungsi .NET. Tapi apa fungsi rata - rata ? Bahkan, dalam .NET Framework metode ini memverifikasi bahwa setidaknya setengah dari ukurannya gratis di stack. Di .NET Core, ia memeriksa 64K gratis.

Sebuah analog juga telah muncul di .NET Core: RuntimeHelpers.TryEnsureSufficientExecutionStack () yang mengembalikan bool, daripada melempar pengecualian.

C # 7.2 memperkenalkan kemampuan untuk menggunakan Span dan stackallock bersama tanpa menggunakan kode yang tidak aman. Mungkin karena ini, stackalloc akan lebih sering digunakan dalam kode dan akan berguna untuk memiliki cara untuk melindungi diri Anda dari StackOverflow saat menggunakannya, memilih tempat untuk mengalokasikan memori. Dengan demikian, diusulkan metode yang memverifikasi kemungkinan alokasi pada stack dan konstruksi trystackalloc .

 Span<byte> span; if (CanAllocateOnStack(size)) span = stackalloc byte[size]; else span = new byte[size]; 

Kembali ke dokumentasi StackOverflow di MSDN

Sebaliknya, ketika stack overflow terjadi dalam aplikasi normal , Common Language Runtime (CLR) mengakhiri proses. "
MSDN

Jika ada aplikasi "normal" yang jatuh selama StackOverflow, lalu ada aplikasi non-normal yang tidak jatuh? Untuk menjawab pertanyaan ini, Anda harus turun dari level aplikasi terkelola ke level CLR.



"Sebuah aplikasi yang meng-host CLR dapat mengubah perilaku default dan menentukan bahwa CLR membongkar domain aplikasi tempat pengecualian terjadi, tetapi membiarkan proses berlanjut." - MSDN
StackOverflowException -> AppDomainUnloadedException

Aplikasi yang meng-host CLR dapat mendefinisikan kembali perilaku stack overflow sehingga alih-alih menyelesaikan seluruh proses, Domain Aplikasi diturunkan, dalam aliran yang terjadi overflow ini. Jadi kita bisa mengubah StackOverflowException menjadi AppDomainUnloadedException.

Ketika aplikasi yang dikelola diluncurkan, runtime .NET secara otomatis dimulai. Tetapi Anda bisa pergi ke arah lain. Misalnya, tulis aplikasi yang tidak dikelola (dalam C ++ atau bahasa lain) yang akan menggunakan API khusus untuk menaikkan CLR dan meluncurkan aplikasi kami. Aplikasi yang menjalankan CLR secara internal akan disebut CLR-host. Dengan menulisnya, kita dapat mengonfigurasi banyak hal dalam runtime. Misalnya, ganti manajer memori dan manajer utas. Kami dalam produksi menggunakan CLR-host untuk menghindari pertukaran halaman memori.

Kode berikut mengonfigurasi host CLR sehingga AppDomain (C ++) diturunkan saat StackOverflow:

 ICLRPolicyManager *policyMgr; pCLRControl->GetCLRManager(IID_ICLRPolicyManager, (void**) (&policyMgr)); policyMgr->SetActionOnFailure(FAIL_StackOverflow, eRudeUnloadAppDomain); 

Apakah ini cara yang baik untuk melarikan diri dari StackOverflow? Mungkin tidak terlalu. Pertama, kami harus menulis kode C ++, yang tidak ingin kami lakukan. Kedua, kita harus mengubah kode C # kita sehingga fungsi yang dapat melempar StackOverflowException dieksekusi di AppDomain terpisah dan di utas terpisah. Kode kami akan segera berubah menjadi mie tersebut:

 try { var appDomain = AppDomain.CreateDomain("..."); appDomain.DoCallBack(() => { var thread = new Thread(() => InfiniteRecursion()); thread.Start(); thread.Join(); }); AppDomain.Unload(appDomain); } catch (AppDomainUnloadedException) { } 

Untuk memanggil metode InfiniteRecursion, kami menulis banyak baris. Ketiga, kami mulai menggunakan AppDomain. Dan ini hampir menjamin banyak masalah baru. Termasuk dengan pengecualian. Pertimbangkan sebuah contoh:

 public class CustomException : Exception {} var appDomain = AppDomain.CreateDomain( "..."); appDomain.DoCallBack(() => throw new CustomException()); System.Runtime.Serialization.SerializationException: Type 'CustomException' is not marked as serializable. at System.AppDomain.DoCallBack(CrossAppDomainDelegate callBackDelegate) 

Karena pengecualian kami tidak ditandai sebagai serializable, kode kami akan turun dengan SerializationException. Dan untuk memperbaiki masalah ini, tidak cukup bagi kita untuk menandai pengecualian kita dengan atribut Serializable, kita masih perlu mengimplementasikan konstruktor tambahan untuk serialisasi.

 [Serializable] public class CustomException : Exception { public CustomException(){} public CustomException(SerializationInfo info, StreamingContext ctx) : base(info, context){} } var appDomain = AppDomain.CreateDomain("..."); appDomain.DoCallBack(() => throw new CustomException()); 

Itu semua ternyata tidak terlalu indah, jadi kita melangkah lebih jauh - ke tingkat sistem operasi dan peretasan, yang seharusnya tidak digunakan dalam produksi.

Seh / kend




Perhatikan bahwa sementara Terkelola-pengecualian terbang antara Terkelola dan CLR, pengecualian SEH terbang antara CLR dan Windows.

SEH - Penanganan Eksepsi Terstruktur

  • Mesin penanganan pengecualian Windows
  • Seragam perangkat lunak dan penanganan pengecualian perangkat keras
  • Pengecualian C # diterapkan di atas SEH

SEH adalah mekanisme penanganan pengecualian di Windows, ini memungkinkan Anda untuk secara merata menangani pengecualian apa pun yang datang, misalnya, dari tingkat prosesor, atau dikaitkan dengan logika aplikasi itu sendiri.

Rantime .NET tahu tentang pengecualian SEH dan dapat mengubahnya menjadi pengecualian yang dikelola:

  • EXCEPTION_STACK_OVERFLOW -> Kecelakaan
  • EXCEPTION_ACCESS_VIOLATION -> AccessViolationException
  • EXCEPTION_ACCESS_VIOLATION -> NullReferenceException
  • EXCEPTION_INT_DIVIDE_BY_ZERO -> DivideByZeroException
  • Pengecualian SEH tidak dikenal -> SEHException

Kita dapat berinteraksi dengan SEH melalui WinApi.

 [DllImport("kernel32.dll")] static extern void RaiseException(uint dwExceptionCode, uint dwExceptionFlags, uint nNumberOfArguments,IntPtr lpArguments); // DivideByZeroException RaiseException(0xc0000094, 0, 0, IntPtr.Zero); // Stack overflow RaiseException(0xc00000fd, 0, 0, IntPtr.Zero); 

Bahkan, konstruksi lemparan juga bekerja melalui SEH.

 throw -> RaiseException(0xe0434f4d, ...) 

Perlu dicatat di sini bahwa kode pengecualian CLR selalu sama, jadi apa pun jenis pengecualian yang kita lemparkan, kode CLR akan selalu diproses.

VEH adalah penanganan pengecualian vektor, perpanjangan dari SEH, tetapi bekerja pada level proses, dan bukan pada level utas tunggal. Jika SEH secara semantik mirip dengan try-catch, maka VEH secara semantik mirip dengan interrupt handler. Kami cukup mengatur penangan kami dan dapat menerima informasi tentang semua pengecualian yang terjadi dalam proses kami. Fitur menarik dari VEH adalah memungkinkan Anda untuk mengubah pengecualian SEH sebelum sampai ke handler.



Kita dapat menempatkan vektor handler kita sendiri antara sistem operasi dan runtime, yang akan menangani pengecualian SEH dan, ketika bertemu EXCEPTION_STACK_OVERFLOW, ubahlah sehingga .NET runtime tidak merusak proses.

Anda dapat berinteraksi dengan VEH melalui WinApi:

 [DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr AddVectoredExceptionHandler(IntPtr FirstHandler,  VECTORED_EXCEPTION_HANDLER VectoredHandler); delegate VEH PVECTORED_EXCEPTION_HANDLER(ref EXCEPTION_POINTERS exceptionPointers); public enum VEH : long { EXCEPTION_CONTINUE_SEARCH = 0, EXCEPTION_EXECUTE_HANDLER = 1, EXCEPTION_CONTINUE_EXECUTION = -1 } delegate VEH PVECTORED_EXCEPTION_HANDLER(ref EXCEPTION_POINTERS exceptionPointers); [StructLayout(LayoutKind.Sequential)] unsafe struct EXCEPTION_POINTERS { public EXCEPTION_RECORD* ExceptionRecord; public IntPtr Context; } delegate VEH PVECTORED_EXCEPTION_HANDLER(ref EXCEPTION_POINTERS exceptionPointers); [StructLayout(LayoutKind.Sequential)] unsafe struct EXCEPTION_RECORD { public uint ExceptionCode; ... } 

Konteks berisi informasi tentang status semua register prosesor pada saat pengecualian. Kami akan tertarik pada EXCEPTION_RECORD dan bidang ExceptionCode di dalamnya. Kita bisa menggantinya dengan kode pengecualian kita sendiri, yang CLR tidak tahu apa-apa tentangnya. Handler vektor terlihat seperti ini:

 static unsafe VEH Handler(ref EXCEPTION_POINTERS e) { if (e.ExceptionRecord == null) return VEH. EXCEPTION_CONTINUE_SEARCH; var record = e. ExceptionRecord; if (record->ExceptionCode != ExceptionStackOverflow) return VEH. EXCEPTION_CONTINUE_SEARCH; record->ExceptionCode = 0x01234567; return VEH. EXCEPTION_EXECUTE_HANDLER; } 

Sekarang kita akan membuat pembungkus yang menginstal handler vektor dalam bentuk metode HandleSO, yang mengambil delegasi yang berpotensi jatuh dari StackOverflowException (untuk kejelasan, kode tidak menangani kesalahan fungsi WinApi dan menghapus handler vektor).

 HandleSO(() => InfiniteRecursion()) ; static T HandleSO<T>(Func<T> action) { Kernel32. AddVectoredExceptionHandler(IntPtr.Zero, Handler); Kernel32.SetThreadStackGuarantee(ref size); try { return action(); } catch (Exception e) when ((uint) Marshal. GetExceptionCode() == 0x01234567) {} return default(T); } HandleSO(() => InfiniteRecursion()); 

Di dalamnya, metode SetThreadStackGuarantee juga digunakan. Metode ini menyimpan ruang tumpukan untuk pemrosesan StackOverflow.

Dengan cara ini, kita bisa selamat dari doa metode dengan rekursi tak terbatas. Aliran kami akan terus berfungsi seolah-olah tidak ada yang terjadi, seolah-olah tidak terjadi luapan.

Tetapi apa yang terjadi jika Anda memanggil HandleSO dua kali di utas yang sama?

 HandleSO(() => InfiniteRecursion()); HandleSO(() => InfiniteRecursion()); 

Dan akan ada AccessViolationException. Kembali ke perangkat tumpukan.


Sistem operasi dapat mendeteksi stack overflows. Di bagian paling atas tumpukan adalah halaman khusus yang ditandai dengan bendera halaman Penjaga. Pertama kali halaman ini diakses, pengecualian lain akan terjadi - STATUS_GUARD_PAGE_VIOLATION, dan bendera halaman Guard akan dihapus dari halaman. Jika Anda cukup mencegat overflow ini, maka halaman ini tidak lagi berada di stack - di overflow berikutnya, sistem operasi tidak akan dapat memahami hal ini dan stack-pointer akan melampaui memori yang dialokasikan untuk stack. Akibatnya, AccessViolationException akan terjadi. Jadi Anda perlu mengembalikan bendera halaman setelah memproses StackOverflow - cara termudah untuk melakukannya adalah dengan menggunakan metode _resetstkoflw dari pustaka runtime C (msvcrt.dll).

 [DllImport("msvcrt.dll")] static extern int _resetstkoflw(); 

Dengan cara yang sama, Anda dapat menangkap AccessViolationException di .NET Core di bawah Windows, yang menyebabkan proses macet. Dalam hal ini, Anda perlu mempertimbangkan urutan penangan vektor dipanggil dan mengatur penangan Anda ke awal rantai, karena .NET Core juga menggunakan VEH saat memproses AccessViolation. Parameter pertama dari fungsi AddVectoredExceptionHandler bertanggung jawab atas urutan penangan yang disebut:

 Kernel32.AddVectoredExceptionHandler(FirstHandler: (IntPtr) 1, handler); 

Setelah mempelajari masalah praktis, kami merangkum hasil umum:

  • Pengecualian tidak sesederhana yang terlihat;
  • Tidak semua pengecualian ditangani dengan cara yang sama;
  • Penanganan pengecualian terjadi pada berbagai tingkat abstraksi;
  • Anda dapat melakukan intervensi dalam proses penanganan pengecualian dan membuat .NET runtime berfungsi berbeda dari yang dimaksudkan.

Referensi


β†’ Repositori dengan contoh-contoh dari laporan
β†’ Dotnext 2016 Moscow - Adam Sitnik - Pengecualian Luar Biasa dalam .NET
β†’ DotNetBook: Pengecualian
β†’ .NET Inside Out Bagian 8 - Menangani Stack Overflow Pengecualian di C # dengan VEH adalah cara lain untuk mencegat StackOverflow.

Pada 22-23 November, Eugene akan berbicara di DotNext 2018 Moskow dengan laporan "Metrik Sistem: Mengumpulkan Kesalahan . " Jeffrey Richter, Greg Young, Pavel Yosifovich dan pembicara lain yang sama menariknya akan datang ke Moskow. Topik-topik laporan dapat dilihat di sini , dan beli tiket di sini . Bergabunglah sekarang!

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


All Articles