الاستثناءات المختلفة في .NET لها خصائصها الخاصة ، وقد يكون من المفيد جدًا التعرف عليها. كيفية خداع CLR؟ كيفية البقاء على قيد الحياة في وقت التشغيل عن طريق اللحاق StackOverflowException؟ ما الاستثناءات التي يبدو من المستحيل التقاطها ، ولكن إذا كنت تريد حقًا ، هل يمكنك ذلك؟

تحت القطع ، نص تقرير يوجين (
epeshk ) Peshkov من مؤتمر
DotNext 2018 Piter ، حيث تحدث عن هذه الميزات وغيرها من الاستثناءات.
مرحبًا اسمي يوجين. أنا أعمل لدى SKB Kontur وأطور نظام استضافة ونشر تطبيقات لـ Windows. خلاصة القول هي أن لدينا العديد من فرق المنتجات التي تكتب خدماتها وتستضيفها معنا. نحن نقدم لهم حل سهل وبسيط لمجموعة متنوعة من مهام البنية التحتية. على سبيل المثال ، لمراقبة استهلاك موارد النظام أو إنهاء النسخ المتماثلة للخدمة.
في بعض الأحيان يتبين أن التطبيقات التي تتم استضافتها على نظامنا تنهار. لقد رأينا العديد من الطرق لكيفية تعطل التطبيق في وقت التشغيل. إحدى هذه الطرق هي التخلص من بعض الاستثناءات غير المتوقعة والساحرة.
اليوم سأتحدث عن ميزات الاستثناءات في .NET. واجهنا بعض هذه الميزات في الإنتاج ، وبعضها في سياق التجارب.
الخطة
- سلوك استثناء .NET
- معالجة استثناءات Windows وعمليات الاختراق
كل ما يلي صحيح بالنسبة لنظام التشغيل Windows. تم اختبار جميع الأمثلة على أحدث إصدار من إطار عمل .NET 4.7.1 الكامل. سيكون هناك أيضًا بعض الإشارات إلى .NET Core.
انتهاك الوصول
يحدث هذا الاستثناء أثناء عمليات الذاكرة غير الصحيحة. على سبيل المثال ، إذا حاول تطبيق الوصول إلى منطقة ذاكرة لا يمكنه الوصول إليها. الاستثناء هو مستوى منخفض ، وعادةً ، إذا حدث ذلك ، فسيكون هناك حاجة إلى تصحيح طويل جدًا.
دعونا نحاول الحصول على هذا الاستثناء باستخدام C #. للقيام بذلك ، سنكتب بايت 42 على العنوان 1000 (نفترض أن 1000 عنوان عشوائي نوعًا ما وعلى الأرجح أن تطبيقنا لا يمكنه الوصول إليه).
try { Marshal.WriteByte((IntPtr) 1000, 42); } catch (AccessViolationException) { ... }
يقوم WriteByte بما نحتاجه فقط: يكتب بايت إلى العنوان المحدد. نتوقع من هذه المكالمة طرح AccessViolationException. سوف يرمي هذا الرمز بالفعل هذا الاستثناء ، وسيكون قادرًا على التعامل معه وسيستمر التطبيق في العمل. الآن دعنا نغير الشفرة قليلاً:
try { var bytes = new byte[] {42}; Marshal.Copy(bytes, 0, (IntPtr) 1000, bytes.Length); } catch (AccessViolationException) { ... }
إذا استخدمت بدلاً من WriteByte أسلوب النسخ ونسخ البايت 42 إلى العنوان 1000 ، ثم باستخدام try-catch ، لا يمكن اكتشاف AccessViolation. في نفس الوقت ، سيتم عرض رسالة على وحدة التحكم تفيد أنه تم إنهاء التطبيق بسبب AccessViolationException غير معالج.
Marshal.Copy(bytes, 0, (IntPtr) 1000, bytes.Length); Marshal.WriteByte((IntPtr) 1000, 42);
اتضح أن لدينا سطرين من التعليمات البرمجية ، بينما يعطل الأول التطبيق بأكمله مع AccessViolation ، والثاني يطرح استثناء معالجًا من نفس النوع. لفهم سبب حدوث ذلك ، سننظر في كيفية ترتيب هذه الأساليب من الداخل.
لنبدأ مع طريقة النسخ.
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);
الشيء الوحيد الذي تفعله طريقة النسخ هو استدعاء طريقة CopyToNative ، التي يتم تنفيذها داخل .NET. إذا استمر تعطل تطبيقنا وحدث استثناء في مكان ما ، فيمكن أن يحدث هذا فقط داخل CopyToNative. من هنا يمكننا عمل الملاحظة الأولى: إذا حدث كود NET يسمى الكود الأصلي و AccessViolation بداخله ، فلن يتمكن كود .NET من معالجة هذا الاستثناء لسبب ما.
الآن سوف نفهم لماذا كان من الممكن معالجة AccessViolation باستخدام أسلوب WriteByte. دعونا نلقي نظرة على كود هذه الطريقة:
unsafe static void WriteByte(IntPtr ptr, byte val) { try { *(byte*) ptr = val; } catch (NullReferenceException) {
يتم تنفيذ هذه الطريقة بالكامل في التعليمات البرمجية المُدارة. يستخدم مؤشر C # لكتابة البيانات إلى العنوان المطلوب ، كما أنه يمسك NullReferenceException. إذا تم اعتراض NRE ، يتم طرح AccessViolationException. لذلك من الضروري بسبب
المواصفات . في هذه الحالة ، يتم التعامل مع جميع الاستثناءات التي تم طرحها بواسطة بنية الرمي. وفقًا لذلك ، في حالة حدوث NullReferenceException أثناء تنفيذ التعليمات البرمجية داخل WriteByte ، يمكننا التقاط AccessViolation. هل يمكن أن تحدث NRE ، في حالتنا ، عند الوصول إلى العنوان 1000 بدلاً من العنوان صفر؟
نعيد كتابة الرمز باستخدام مؤشرات C # مباشرة ، ونرى أنه عند الوصول إلى عنوان غير صفري ، يتم طرح NullReferenceException فعليًا:
*(byte*) 1000 = 42;
لفهم سبب حدوث ذلك ، نحتاج إلى تذكر كيفية عمل ذاكرة العملية. في ذاكرة العملية ، جميع العناوين افتراضية. هذا يعني أن التطبيق يحتوي على مساحة عنوان كبيرة ويتم عرض بعض الصفحات منه فقط في الذاكرة الفعلية الحقيقية. ولكن هناك ميزة: أول 64 كيلوبايت من العناوين لا يتم تعيينها أبدًا للذاكرة الفعلية ولا يتم إعطاؤها للتطبيق. يعرف Rantime .NET هذا ويستخدمه. إذا حدث AccessViolation في التعليمات البرمجية المُدارة ، فإن وقت التشغيل يتحقق من العنوان الذي تم الوصول إليه في الذاكرة ويولد استثناء مناسبًا. للعناوين من 0 إلى 2 ^ 16 - NullReference ، لجميع الآخرين - AccessViolation.

دعونا نرى لماذا يتم طرح NullReference ليس فقط عند الوصول إلى العنوان صفر. تخيل أنك تصل إلى حقل كائن من النوع المرجعي وأن المرجع إلى هذا الكائن فارغ:

في هذه الحالة ، نتوقع الحصول على NullReferenceException. يحدث الوصول إلى حقل كائن بواسطة الإزاحة نسبة إلى عنوان هذا الكائن. اتضح أننا سننتقل إلى عنوان قريب بما يكفي من الصفر (تذكر أن الرابط إلى كائننا الأصلي هو صفر). مع سلوك وقت التشغيل هذا ، نحصل على الاستثناء المتوقع دون التحقق الإضافي من عنوان الكائن نفسه.
ولكن ماذا يحدث إذا انتقلنا إلى حقل كائن ، وكان هذا الكائن نفسه يستهلك أكثر من 64 كيلوبايت؟

هل يمكننا الحصول على AccessViolation في هذه الحالة؟ لنقم بتجربة. لنقم بإنشاء كائن كبير جدًا وسنشير إلى حقوله. حقل واحد في بداية الكائن ، والثاني في النهاية:

ستقوم كلتا الطريقتين بإلقاء NullReferenceException. لن يحدث AccessViolationException.
دعونا نلقي نظرة على التعليمات التي سيتم إنشاؤها لهذه الأساليب. في الحالة الثانية ، أضاف المترجم JIT تعليمة cmp إضافية تصل إلى عنوان الكائن نفسه ، وبالتالي استدعاء AccessViolation بعنوان صفر ، والذي سيتم تحويله بواسطة وقت التشغيل إلى NullReferenceException.
تجدر الإشارة إلى أنه في هذه التجربة لا يكفي استخدام مصفوفة ككائن كبير. لماذا؟ اترك هذا السؤال للقارئ ، اكتب الأفكار في التعليقات :)
دعونا نلخص التجارب مع AccessViolation.

يتصرف AccessViolationException بشكل مختلف اعتمادًا على مكان حدوث الاستثناء (في التعليمات البرمجية المُدارة أو في اللغة الأم). بالإضافة إلى ذلك ، إذا حدث استثناء في التعليمات البرمجية المُدارة ، فسيتم التحقق من عنوان الكائن.
السؤال هو: هل يمكننا التعامل مع AccessViolationException التي حدثت في التعليمات البرمجية الأصلية أو في التعليمات البرمجية المُدارة ، ولكن لم يتم تحويلها إلى NullReference ولا يتم طرحها باستخدام الرمي؟ هذه في بعض الأحيان ميزة مفيدة ، خاصة عند العمل مع رمز غير آمن. تعتمد الإجابة على هذا السؤال على إصدار .NET.

في .NET 1.0 ، لم يكن هناك AccessViolationException على الإطلاق. تم اعتبار جميع الروابط صالحة أو خالية. بحلول وقت .NET 2.0 ، أصبح من الواضح أنه بدون العمل المباشر مع الذاكرة - لا توجد طريقة ، وظهرت AccessViolation ، بينما كانت قابلة للمعالجة. في الإصدار 4.0 وما فوق ، لا يزال قابلاً للتطبيق ، ولكن معالجته ليست بهذه البساطة. لالتقاط هذا الاستثناء ، تحتاج الآن إلى وضع علامة على الطريقة التي توجد بها كتلة الالتقاط بالسمة HandleProcessCorruptedStateException. على ما يبدو ، قام المطورون بذلك لأنهم اعتقدوا أن AccessViolationException لم يكن الاستثناء الذي يجب اكتشافه في التطبيق العادي.
بالإضافة إلى ذلك ، من أجل التوافق مع الإصدارات السابقة ، من الممكن استخدام إعدادات وقت التشغيل:
- legacyNullReferenceExceptionPolicy يُرجع سلوك .NET 1.0 - تتحول جميع مركبات AV إلى NRE
- legacyCorruptedStateExceptionsPolicy تُرجع سلوك .NET 2.0 - يتم اعتراض جميع مركبات AV
في .NET ، لا يتم التعامل مع Core AccessViolation على الإطلاق.
في إنتاجنا كان هناك مثل هذا الوضع:

استخدم تطبيق تم إنشاؤه بموجب .NET 4.7.1 مكتبة تعليمات برمجية مشتركة تم إنشاؤها بموجب .NET 3.5. كان هناك مساعد في هذه المكتبة لتشغيل إجراء دوري:
while (isRunning) { try { action(); } catch (Exception e) { log.Error(e); } WaitForNextExecution(... ); }
مررنا من تطبيقنا إلى هذا المساعد. حدث ذلك أنه تحطم مع AccessViolation. نتيجة لذلك ، سجل تطبيقنا باستمرار AccessViolation ، بدلاً من تعطله بسبب الكود في المكتبة تحت 3.5 يمكن أن يمسك به. وتجدر الإشارة إلى أن الاعتراض لا يعتمد على إصدار وقت التشغيل الذي يتم تشغيل التطبيق عليه ، ولكن على TargetFramework ، الذي تم إنشاء التطبيق بموجبه ، وتبعياته.
لتلخيص. تعتمد معالجة AccessVilolation على مكان نشأتها - في التعليمات البرمجية الأصلية أو المُدارة - بالإضافة إلى إعدادات TargetFramework ووقت التشغيل.
إحباط الموضوع
في بعض الأحيان في الكود تحتاج إلى إيقاف تنفيذ أحد سلاسل المحادثات. للقيام بذلك ، يمكنك استخدام مؤشر الترابط. Abort ()؛
var thread = new Thread(() => { try { ... } catch (ThreadAbortException e) { ... Thread.ResetAbort(); } }); ... thread.Abort();
عندما يتم استدعاء الأسلوب إحباط في مؤشر ترابط توقف ، يتم طرح ThreadAbortException. دعونا نحلل ميزاته. على سبيل المثال ، رمز مثل هذا:
var thread = new Thread(() => { try { … } catch (ThreadAbortException e) { … } }); ... thread.Abort();
ما يعادل هذا تماما:
var thread = new Thread(() => { try { ... } catch (ThreadAbortException e) { ... throw; } }); ... thread.Abort();
إذا كنت لا تزال بحاجة إلى معالجة ThreadAbort وتنفيذ بعض الإجراءات الأخرى في سلسلة المحادثات المتوقفة ، فيمكنك استخدام طريقة Thread.ResetAbort () ؛ إنه يوقف عملية إيقاف التدفق ويتوقف الاستثناء عن الرمي أعلى المكدس. من المهم أن نفهم أن أسلوب thread.Abort () نفسه لا يضمن أي شيء - قد يمنعه الرمز الموجود في مؤشر الترابط الذي تم إيقافه.
ميزة أخرى في thread.Abort () هي أنه لن يكون قادرًا على مقاطعة الرمز إذا كان موجودًا في النهاية وأخيرًا يتم حظره.
داخل كود الإطار ، يمكنك غالبًا العثور على طرق يكون فيها كتلة المحاولة فارغة ويكون كل المنطق بداخلها أخيرًا. يتم ذلك فقط لمنع طرح هذا الرمز بواسطة ThreadAbortException.
أيضاً ، ينتظر استدعاء الأسلوب thread.Abort () لـ ThreadAbortException ليتم طرحه. اجمع بين هاتين الحقائقين واحصل على أن طريقة thread.Abort () يمكن أن تحظر مؤشر الترابط.
var thread = new Thread(() => { try { } catch { }
في الواقع ، يمكن مواجهة ذلك عند استخدام الاستخدام. يتم نشرها في محاولة / أخيرا ، في النهاية ، تسمى طريقة التخلص. يمكن أن يكون معقدًا بشكل تعسفي ، ويحتوي على معالجات الأحداث ، استخدم الأقفال. وإذا تم استدعاء thread.Abort في وقت التشغيل ، فإن Dispose - thread.Abort () سينتظرها. لذا نحصل على قفل من الصفر تقريبًا.
في .NET Core ، يطرح أسلوب thread.Abort () PlatformNotSupportedException. وأعتقد أن هذا أمر جيد للغاية ، لأنه يحفز استخدام ليس الخيط. Abort () ، ولكن الأساليب غير الغازية لوقف تنفيذ التعليمات البرمجية ، على سبيل المثال استخدام CancellationToken.
خارج الذاكرة
يمكن الحصول على هذا الاستثناء إذا كانت الذاكرة الموجودة على الجهاز أقل من المطلوب. أو عندما واجهنا قيود عملية 32 بت. ولكن يمكنك الحصول عليها حتى إذا كان الكمبيوتر يحتوي على الكثير من الذاكرة الفارغة ، والعملية 64 بت.
var arr4gb = new int[int.MaxValue/2];
سوف يرمي الرمز أعلاه OutOfMemory. الشيء هو أنه ، بشكل افتراضي ، لا يُسمح بالكائنات الأكبر من 2 غيغابايت. يمكن إصلاح ذلك عن طريق تعيين gcAllowVeryLargeObjects في App.config. في هذه الحالة ، يتم إنشاء صفيف 4 غيغابايت.
الآن دعنا نحاول إنشاء مصفوفة أكثر.
var largeArr = new int[int.MaxValue];
الآن لن تساعد حتى gcAllowVeryLargeObjects. وذلك لأن .NET لديها
حد على الفهرس الأقصى في المصفوفة . هذا التقييد أقل من int.MaxValue.
مؤشر الصفيف ماكس:
- صفائف بايت - 0x7FFFFFC7
- صفائف أخرى - 0X7F E FFFFF
في هذه الحالة ، سيحدث OutOfMemoryException ، على الرغم من أننا واجهنا في الواقع قيودًا على نوع البيانات ، وليس نقصًا في الذاكرة.
في بعض الأحيان يتم التخلص من OutOfMemory بشكل واضح من خلال التعليمات البرمجية المُدارة داخل .NET framework:

هذا هو تنفيذ أسلوب string.Concat. إذا كان طول سلسلة النتائج أكبر من int.MaxValue ، يتم طرح OutOfMemoryException على الفور.
دعنا ننتقل إلى الحالة عندما تنشأ OutOfMemory في حالة نفاد الذاكرة بالفعل.
LimitMemory(64.Mb()); try { while (true) list.Add(new byte[size]); } catch (OutOfMemoryException e) { Console.WriteLine(e); }
أولاً ، نقصر ذاكرة العملية على 64 ميغابايت. بعد ذلك ، داخل الحلقة ، حدد صفائف بايت جديدة ، واحفظها في بعض الورقة بحيث لا يجمعها GC ، وحاول التقاط OutOfMemory.
في هذه الحالة ، يمكن أن يحدث أي شيء:
- تمت معالجة الاستثناء
- ستنهار العملية
- دعنا نذهب ، ولكن الاستثناء سيتعطل مرة أخرى
- دعنا نذهب ، ولكن StackOverflow سوف يتعطل
في هذه الحالة ، لن يكون البرنامج نهائيًا. دعونا نحلل جميع الخيارات:
- يمكن معالجة الاستثناء. داخل .NET ، لا يوجد شيء يمنعك من التعامل مع OutOfMemoryException.
- قد تسقط العملية. لا تنس أن لدينا تطبيق مُدار. هذا يعني أنه لا يتم تنفيذ التعليمات البرمجية داخله فقط ، ولكن أيضًا رمز وقت التشغيل. على سبيل المثال ، GC. وبالتالي ، قد يحدث موقف عندما يريد وقت التشغيل تخصيص ذاكرة لنفسه ، ولكن لا يمكنه القيام بذلك ، فلن نتمكن من التقاط الاستثناء.
- دعنا نذهب إلى الصيد ، لكن الاستثناء سيتعطل مرة أخرى. داخل المصيد ، نقوم أيضًا بالمهمة حيث نحتاج إلى ذاكرة (نقوم بطباعة استثناء لوحدة التحكم) ، وهذا يمكن أن يتسبب في استثناء جديد.
- دعنا نذهب ، ولكن StackOverflow سوف يتعطل. يحدث StackOverflow نفسه عندما يتم استدعاء أسلوب WriteLine ، ولكن لا يوجد تجاوز سعة مكدس هنا ، ولكن يحدث موقف مختلف. دعونا نحلل ذلك بمزيد من التفصيل.

في الذاكرة الافتراضية ، لا يمكن فقط تعيين الصفحات على الذاكرة الفعلية ، ولكن يمكن أيضًا حجزها. إذا كانت الصفحة محجوزة ، فقد لاحظ التطبيق أنها ستستخدمها. إذا تم تعيين الصفحة بالفعل على ذاكرة حقيقية أو مبادلة ، فعندئذٍ تسمى "ملتزم" (ملتزم). يستخدم المكدس هذه القدرة لتقسيم الذاكرة إلى محجوزة وملتزمة. يبدو شيء مثل هذا:

اتضح أننا نسمي طريقة WriteLine ، التي تأخذ مكانًا ما على المكدس. اتضح أن كل الذاكرة المخصصة قد انتهت بالفعل ، مما يعني أن نظام التشغيل في هذه اللحظة يجب أن يأخذ صفحة محجوزة أخرى على المكدس ويعينها على ذاكرة فعلية حقيقية ، مليئة بالفعل بمصفوفات بايت. هذا يؤدي إلى استثناء StackOverflow.
سيسمح لك الكود التالي بتخصيص كل الذاكرة إلى المكدس في بداية الدفق في وقت واحد.
new Thread(() => F(), 4*1024*1024).Start();
وبدلاً من ذلك ، يمكنك استخدام
إعداد وقت التشغيل disableCommitThreadStack. يجب تعطيله حتى يتم تنفيذ مكدس مؤشر الترابط مقدمًا. تجدر الإشارة إلى أن السلوك الافتراضي الموصوف في التوثيق والملاحظ في الواقع مختلف.

تجاوز سعة المكدس
دعونا نلقي نظرة فاحصة على StackOverflowException. دعونا نلقي نظرة على مثالين من التعليمات البرمجية. في أحدها ، نقوم بتشغيل العودية اللانهائية ، مما يؤدي إلى تجاوز سعة المكدس ، في الثانية نرمي هذا الاستثناء برمية.
try { InfiniteRecursion(); } catch (Exception) { ... }
try { throw new StackOverflowException(); } catch (Exception) { ... }
نظرًا لأن جميع الاستثناءات التي تم طرحها برمية يتم التعامل معها ، في الحالة الثانية سنلتقط الاستثناء. ومع الحالة الأولى ، كل شيء أكثر إثارة للاهتمام. أنتقل إلى
MSDN :
"لا يمكنك التقاط استثناءات تجاوز سعة المكدس ، لأن كود معالجة الاستثناء قد يتطلب المكدس."
MSDN
تقول هنا أننا لن نتمكن من التقاط StackOverflowException ، لأن الاعتراض نفسه قد يتطلب مساحة مكدس إضافية قد انتهت بالفعل.
للحماية بطريقة ما من هذا الاستثناء ، يمكننا القيام بما يلي. أولاً ، يمكنك تحديد عمق العودية. ثانيًا ، يمكنك استخدام أساليب فئة RuntimeHelpers:
RuntimeHelpers.EnsureSufficientExecutionStack () ؛
- "يضمن أن مساحة المكدس المتبقية كبيرة بما يكفي لتنفيذ متوسط وظيفة .NET Framework." - MSDN
- InsufficientExecutionStackException
- 512 كيلوبايت - x86 ، AnyCPU ، 2 ميجابايت - x64 (نصف حجم المكدس)
- 64/128 كيلوبايت - .NET Core
- تحقق من مساحة عنوان المكدس فقط
توضح وثائق هذه الطريقة أنها تتحقق من وجود مساحة كافية على المكدس لتنفيذ
متوسط وظيفة .NET. ولكن ما هي الوظيفة
المتوسطة ؟ في الواقع ، في .NET Framework ، تتحقق هذه الطريقة من أن نصف حجمها على الأقل مجاني على المكدس. في .NET Core ، يتحقق من وجود 64 كيلو بايت مجانًا.
وقد ظهر أيضًا تناظري في .NET Core: RuntimeHelpers.TryEnsureSufficientExecutionStack () الذي يعيد منطقية ، بدلاً من إلقاء استثناء.
قدم C # 7.2 القدرة على استخدام Span و stackallock معًا دون استخدام رمز غير آمن. ربما بسبب هذا ، سيتم استخدام stackalloc بشكل أكثر تكرارًا في التعليمات البرمجية وسيكون من المفيد أن يكون لديك طريقة لحماية نفسك من StackOverflow عند استخدامه ، واختيار مكان تخصيص الذاكرة. وبهذه الطريقة ، يتم اقتراح طريقة
للتحقق من إمكانية التخصيص على المكدس وبناء
trystackalloc .
Span<byte> span; if (CanAllocateOnStack(size)) span = stackalloc byte[size]; else span = new byte[size];
العودة إلى وثائق StackOverflow على MSDN
بدلاً من ذلك ، عندما يحدث تجاوز سعة مكدس في تطبيق عادي ، ينهي وقت تشغيل اللغة العامة (CLR) العملية ".
MSDN
إذا كان هناك تطبيق "عادي" يقع أثناء StackOverflow ، فهناك تطبيقات غير عادية لا تقع؟ للإجابة على هذا السؤال ، سيكون عليك النزول إلى مستوى من مستوى التطبيق المُدار إلى مستوى CLR.

"يمكن للتطبيق الذي يستضيف CLR تغيير السلوك الافتراضي وتحديد أن CLR يقوم بإلغاء تحميل مجال التطبيق حيث يحدث الاستثناء ، ولكنه يتيح استمرار العملية." - MSDN
StackOverflowException -> AppDomainUnloadedException
يمكن للتطبيق الذي يستضيف CLR إعادة تعريف سلوك تجاوز سعة المكدس بحيث بدلاً من إكمال العملية بأكملها ، يتم إلغاء تحميل مجال التطبيق ، في الدفق الذي حدث فيه تجاوز الحد الأقصى. حتى نتمكن من تحويل StackOverflowException إلى AppDomainUnloadedException.
عند تشغيل تطبيق مُدار ، يبدأ وقت تشغيل .NET تلقائيًا. ولكن يمكنك الذهاب في الاتجاه الآخر. على سبيل المثال ، اكتب تطبيقًا غير مُدار (في لغة C ++ أو لغة أخرى) سيستخدم واجهة برمجة تطبيقات خاصة من أجل رفع CLR وتشغيل تطبيقنا. التطبيق الذي يقوم بتشغيل CLR داخليًا سيسمى CLR-host. من خلال كتابته ، يمكننا تكوين العديد من الأشياء في وقت التشغيل. على سبيل المثال ، استبدل مدير الذاكرة ومدير سلسلة المحادثات. نحن في الإنتاج نستخدم CLR-host لتجنب تبديل صفحات الذاكرة.
يكوّن الرمز التالي مضيف CLR بحيث يتم إلغاء تحميل AppDomain (C ++) أثناء StackOverflow:
ICLRPolicyManager *policyMgr; pCLRControl->GetCLRManager(IID_ICLRPolicyManager, (void**) (&policyMgr)); policyMgr->SetActionOnFailure(FAIL_StackOverflow, eRudeUnloadAppDomain);
هل هذه طريقة جيدة للهروب من StackOverflow؟ ربما ليس كذلك. أولاً ، كان علينا كتابة رمز C ++ ، والذي لا نريد القيام به. ثانيًا ، يجب علينا تغيير رمز C # الخاص بنا بحيث يتم تنفيذ الوظيفة التي يمكنها رمي StackOverflowException في AppDomain منفصل وفي سلسلة محادثات منفصلة. سيتحول رمزنا على الفور إلى مثل هذه المكرونة:
try { var appDomain = AppDomain.CreateDomain("..."); appDomain.DoCallBack(() => { var thread = new Thread(() => InfiniteRecursion()); thread.Start(); thread.Join(); }); AppDomain.Unload(appDomain); } catch (AppDomainUnloadedException) { }
من أجل استدعاء طريقة InfiniteRecursion ، كتبنا مجموعة من الخطوط. ثالثًا ، بدأنا في استخدام AppDomain. وهذا يكفل مجموعة من المشاكل الجديدة. بما في ذلك مع الاستثناءات. فكر في مثال:
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)
نظرًا لأن استثناءنا لم يتم وضع علامة عليه على أنه قابل للتسلسل ، فسيتم إسقاط رمزنا مع SerializationException. ولإصلاح هذه المشكلة ، لا يكفي أن نميز استثناءنا بسمة Serializable ، فما زلنا بحاجة إلى تطبيق مُنشئ إضافي للتسلسل.
[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());
كل شيء ليس جميلًا جدًا ، لذلك نذهب إلى أبعد من ذلك - إلى مستوى نظام التشغيل والاختراق ، والذي لا ينبغي استخدامه في الإنتاج.
Seh / veh

لاحظ أنه بينما تطير الاستثناءات المُدارة بين المُدار و CLR ، تنتقل استثناءات SEH بين CLR و Windows.
SEH - معالجة الاستثناءات المركبة
- محرك معالجة استثناء Windows
- معالجة موحدة للبرامج والأجهزة
- تم تنفيذ استثناءات C # أعلى SEH
SEH هي آلية لمعالجة الاستثناءات في Windows ، فهي تسمح لك بالتعامل بشكل متساوٍ مع أي استثناءات جاءت ، على سبيل المثال ، من مستوى المعالج ، أو كانت مرتبطة بمنطق التطبيق نفسه.
يعرف Rantime .NET عن استثناءات SEH ويمكنه تحويلها إلى استثناءات مُدارة:
- EXCEPTION_STACK_OVERFLOW -> تحطم
- EXCEPTION_ACCESS_VIOLATION -> AccessViolationException
- EXCEPTION_ACCESS_VIOLATION -> NullReferenceException
- EXCEPTION_INT_DIVIDE_BY_ZERO -> DivideByZeroException
- استثناءات SEH غير معروفة -> SEHException
يمكننا التفاعل مع SEH من خلال WinApi.
[DllImport("kernel32.dll")] static extern void RaiseException(uint dwExceptionCode, uint dwExceptionFlags, uint nNumberOfArguments,IntPtr lpArguments);
في الواقع ، يعمل هيكل الرمي أيضًا من خلال SEH. throw -> RaiseException(0xe0434f4d, ...)
تجدر الإشارة هنا إلى أن رمز استثناء CLR هو نفسه دائمًا ، لذلك بغض النظر عن نوع الاستثناء الذي نطرحه ، ستتم معالجته دائمًا.VEH هو معالجة استثناء متجه ، وهو امتداد لـ SEH ، ولكنه يعمل على مستوى العملية ، وليس على مستوى مؤشر ترابط واحد. إذا كان SEH مشابهًا لغويًا لتجربة الالتقاط ، فإن VEH يشبه إلى حد ما معالج المقاطعة. نقوم ببساطة بتعيين معالجنا ويمكننا تلقي معلومات حول جميع الاستثناءات التي تحدث في عمليتنا. ميزة مثيرة للاهتمام في VEH هي أنها تسمح لك بتغيير استثناء SEH قبل أن تصل إلى المعالج.
يمكننا وضع معالج المتجه الخاص بنا بين نظام التشغيل ووقت التشغيل ، والذي سيتعامل مع استثناءات SEH ، وعندما يواجه EXCEPTION_STACK_OVERFLOW ، قم بتغييره بحيث لا يعطل وقت تشغيل .NET العملية.يمكنك التفاعل مع VEH من خلال 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; ... }
يحتوي السياق على معلومات حول حالة كافة سجلات المعالج في وقت الاستثناء. سنكون مهتمين بـ EXCEPTION_RECORD وحقل ExceptionCode فيه. يمكننا استبداله برمز الاستثناء الخاص بنا ، والذي لا يعرف CLR عنه شيئًا. يبدو معالج المتجهات كالتالي: 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; }
سننشئ الآن مجمِّعًا يثبت معالج متجه في شكل طريقة HandleSO ، والذي يستوعب مفوضًا من المحتمل أن يسقط من StackOverflowException (للتوضيح ، لا يتعامل الرمز مع أخطاء وظيفة WinApi وإزالة معالج المتجه). 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());
داخله ، يتم أيضًا استخدام طريقة SetThreadStackGuarantee. تحجز هذه الطريقة مساحة مكدس لمعالجة StackOverflow.بهذه الطريقة ، يمكننا البقاء على قيد الحياة باستدعاء طريقة مع العودية اللانهائية. سيستمر البث لدينا في العمل كما لو لم يحدث شيء ، كما لو لم يحدث تجاوز.ولكن ماذا يحدث إذا اتصلت بـ HandleSO مرتين في نفس الموضوع؟ HandleSO(() => InfiniteRecursion()); HandleSO(() => InfiniteRecursion());
وسيكون هناك AccessViolationException. العودة إلى جهاز المكدس.
يمكن لنظام التشغيل الكشف عن تجاوزات المكدس. في أعلى المكدس توجد صفحة خاصة تحمل علامة صفحة الحماية. في المرة الأولى التي يتم فيها الوصول إلى هذه الصفحة ، سيحدث استثناء آخر - STATUS_GUARD_PAGE_VIOLATION ، وستتم إزالة علامة صفحة الحماية من الصفحة. إذا قمت ببساطة باعتراض هذا الفائض ، فلن تكون هذه الصفحة على المكدس - في التدفق الفائض التالي ، لن يتمكن نظام التشغيل من فهم هذا وسيتجاوز مؤشر المكدس الذاكرة المخصصة للمكدس. نتيجة لذلك ، سيحدث AccessViolationException. لذلك تحتاج إلى استعادة علامات الصفحة بعد معالجة StackOverflow - أسهل طريقة للقيام بذلك هي استخدام طريقة _resetstkoflw من مكتبة وقت التشغيل C (msvcrt.dll). [DllImport("msvcrt.dll")] static extern int _resetstkoflw();
بطريقة مماثلة ، يمكنك التقاط AccessViolationException في .NET Core ضمن Windows ، مما يؤدي إلى تعطل العملية. في هذه الحالة ، يجب أن تأخذ في الاعتبار الترتيب الذي يتم فيه استدعاء معالجات المتجهات وتعيين المعالج الخاص بك على بداية السلسلة ، حيث يستخدم .NET Core أيضًا VEH عند معالجة AccessViolation. تكون المعلمة الأولى للدالة AddVectoredExceptionHandler مسؤولة عن الترتيب الذي يتم من خلاله استدعاء المعالجات: Kernel32.AddVectoredExceptionHandler(FirstHandler: (IntPtr) 1, handler);
بعد دراسة القضايا العملية ، نلخص النتائج العامة:- الاستثناءات ليست بسيطة كما تبدو ؛
- لا يتم التعامل مع جميع الاستثناءات بنفس الطريقة ؛
- تحدث معالجة الاستثناءات على مستويات مختلفة من التجريد ؛
- يمكنك التدخل في عملية معالجة الاستثناءات وجعل وقت تشغيل .NET يعمل بشكل مختلف عن الغرض الأصلي.
المراجع
→ مستودع مع أمثلة من التقرير→ Dotnext 2016 موسكو - آدم سيتنيك - استثناءات استثنائية في .NET→ DotNetBook: استثناءات→ .NET Inside Out Part 8 - التعامل مع استثناء تجاوز سعة المكدس في C # مع VEH هو طريقة أخرى لاعتراض StackOverflow.من 22 إلى 23 نوفمبر ، سيتحدث يوجين في DotNext 2018 موسكو بتقرير "مقاييس النظام: جمع العثرات" . سيأتي إلى موسكو جيفري ريشتر ، جريج يونج ، بافل يوسيفوفيتش وغيرهم من المتحدثين المثيرين للاهتمام. يمكن الاطلاع على مواضيع التقارير هنا ، وشراء التذاكر هنا . انضم الآن!