Die verschiedenen Ausnahmen in .NET haben ihre eigenen Eigenschaften, und es kann sehr nützlich sein, sie zu kennen. Wie betrüge ich die CLR? Wie kann man zur Laufzeit am Leben bleiben, indem man eine StackOverflowException abfängt? Welche Ausnahmen scheint es unmöglich zu fangen, aber wenn Sie wirklich wollen, können Sie?

Unter dem Schnitt das Transkript des Berichts von Eugene (
epeshk ) Peshkov von unserer
DotNext 2018 Piter- Konferenz, wo er über diese und andere Merkmale von Ausnahmen sprach.
Hallo! Ich heiße Eugene. Ich arbeite für SKB Kontur und entwickle ein Hosting-System und stelle Anwendungen für Windows bereit. Das Fazit ist, dass wir viele Produktteams haben, die ihre eigenen Services schreiben und sie bei uns hosten. Wir bieten ihnen eine einfache Lösung für eine Vielzahl von Infrastrukturaufgaben. Zum Beispiel, um den Verbrauch von Systemressourcen zu überwachen oder Replikate für den Dienst zu beenden.
Manchmal stellt sich heraus, dass die auf unserem System gehosteten Anwendungen auseinanderfallen. Wir haben so viele Möglichkeiten gesehen, wie eine Anwendung zur Laufzeit abstürzen kann. Eine dieser Methoden besteht darin, eine unerwartete und bezaubernde Ausnahme auszuschließen.
Heute werde ich über die Funktionen von Ausnahmen in .NET sprechen. Wir haben einige dieser Merkmale in der Produktion und einige im Verlauf von Experimenten festgestellt.
Planen
- .NET-Ausnahmeverhalten
- Windows-Ausnahmebehandlung und Hacks
Alle folgenden Aussagen gelten für Windows. Alle Beispiele wurden mit der neuesten Version des vollständigen .NET 4.7.1-Frameworks getestet. Es wird auch einige Verweise auf .NET Core geben.
Zugriffsverletzung
Diese Ausnahme tritt bei falschen Speicheroperationen auf. Wenn eine Anwendung beispielsweise versucht, auf einen Speicherbereich zuzugreifen, auf den sie keinen Zugriff hat. Die Ausnahme ist ein niedriges Level, und normalerweise ist in diesem Fall ein sehr langes Debugging erforderlich.
Versuchen wir, diese Ausnahme mit C # zu erhalten. Dazu schreiben wir Byte 42 an die Adresse 1000 (wir gehen davon aus, dass 1000 eine ziemlich zufällige Adresse ist und unsere Anwendung höchstwahrscheinlich keinen Zugriff darauf hat).
try { Marshal.WriteByte((IntPtr) 1000, 42); } catch (AccessViolationException) { ... }
WriteByte macht genau das, was wir brauchen: Es schreibt ein Byte an die angegebene Adresse. Wir erwarten, dass dieser Aufruf eine AccessViolationException auslöst. Dieser Code löst tatsächlich diese Ausnahme aus, kann sie verarbeiten und die Anwendung funktioniert weiterhin. Jetzt ändern wir den Code ein wenig:
try { var bytes = new byte[] {42}; Marshal.Copy(bytes, 0, (IntPtr) 1000, bytes.Length); } catch (AccessViolationException) { ... }
Wenn Sie anstelle von WriteByte die Copy-Methode verwenden und Byte 42 an die Adresse 1000 kopieren, kann AccessViolation mit try-catch nicht abgefangen werden. Gleichzeitig wird auf der Konsole eine Meldung angezeigt, dass die Anwendung aufgrund einer nicht behandelten AccessViolationException beendet wurde.
Marshal.Copy(bytes, 0, (IntPtr) 1000, bytes.Length); Marshal.WriteByte((IntPtr) 1000, 42);
Es stellt sich heraus, dass wir zwei Codezeilen haben, während die erste die gesamte Anwendung mit AccessViolation zum Absturz bringt und die zweite eine verarbeitete Ausnahme desselben Typs auslöst. Um zu verstehen, warum dies geschieht, werden wir uns ansehen, wie diese Methoden von innen angeordnet sind.
Beginnen wir mit der Copy-Methode.
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);
Die Copy-Methode ruft lediglich die in .NET implementierte CopyToNative-Methode auf. Wenn unsere Anwendung immer noch abstürzt und irgendwo eine Ausnahme auftritt, kann dies nur in CopyToNative geschehen. Von hier aus können wir die erste Beobachtung machen: Wenn der .NET-Code als nativer Code bezeichnet wird und AccessViolation darin vorkommt, kann der .NET-Code diese Ausnahme aus irgendeinem Grund nicht behandeln.
Jetzt werden wir verstehen, warum es möglich war, AccessViolation mit der WriteByte-Methode zu verarbeiten. Schauen wir uns den Code für diese Methode an:
unsafe static void WriteByte(IntPtr ptr, byte val) { try { *(byte*) ptr = val; } catch (NullReferenceException) {
Diese Methode ist vollständig in verwaltetem Code implementiert. Es verwendet den C # -Pointer, um Daten an die gewünschte Adresse zu schreiben, und fängt auch eine NullReferenceException ab. Wenn die NRE abgefangen wird, wird eine AccessViolationException ausgelöst. Es ist also wegen der
Spezifikation notwendig. In diesem Fall werden alle vom throw-Konstrukt ausgelösten Ausnahmen behandelt. Wenn während der Codeausführung in WriteByte eine NullReferenceException auftritt, können wir AccessViolation abfangen. Könnte in unserem Fall ein NRE auftreten, wenn auf die Adresse 1000 anstatt auf die Adresse Null zugegriffen wird?
Wir schreiben den Code mithilfe von C # -Zeigern direkt neu und stellen fest, dass beim Zugriff auf eine Adresse ungleich Null tatsächlich eine NullReferenceException ausgelöst wird:
*(byte*) 1000 = 42;
Um zu verstehen, warum dies geschieht, müssen wir uns daran erinnern, wie der Speicher des Prozesses funktioniert. Im Prozessspeicher sind alle Adressen virtuell. Dies bedeutet, dass die Anwendung einen großen Adressraum hat und nur einige Seiten davon im realen physischen Speicher angezeigt werden. Es gibt jedoch eine Besonderheit: Die ersten 64 KB Adressen werden niemals dem physischen Speicher zugeordnet und nicht an die Anwendung übergeben. Rantime .NET weiß das und verwendet es. Wenn AccessViolation im verwalteten Code aufgetreten ist, überprüft die Laufzeit, auf welche Adresse im Speicher zugegriffen wurde, und generiert eine entsprechende Ausnahme. Für Adressen von 0 bis 2 ^ 16 - NullReference, für alle anderen - AccessViolation.

Mal sehen, warum die NullReference nicht nur beim Zugriff auf die Nulladresse ausgelöst wird. Stellen Sie sich vor, Sie greifen auf ein Feld eines Objekts eines Referenztyps zu und der Verweis auf dieses Objekt ist null:

In dieser Situation erwarten wir eine NullReferenceException. Der Zugriff auf das Feld des Objekts erfolgt in einem Versatz relativ zur Adresse dieses Objekts. Es stellt sich heraus, dass wir uns an eine Adresse wenden, die nahe genug bei Null liegt (denken Sie daran, dass der Link zu unserem ursprünglichen Objekt Null ist). Mit diesem Laufzeitverhalten erhalten wir die erwartete Ausnahme ohne zusätzliche Überprüfung der Adresse des Objekts selbst.
Aber was passiert, wenn wir uns dem Feld eines Objekts zuwenden und dieses Objekt selbst mehr als 64 KB belegt?

Können wir in diesem Fall AccessViolation erhalten? Lass uns ein Experiment machen. Lassen Sie uns ein sehr großes Objekt erstellen und auf seine Felder verweisen. Ein Feld am Anfang des Objekts, das zweite am Ende:

Beide Methoden lösen eine NullReferenceException aus. Es tritt keine AccessViolationException auf.
Schauen wir uns die Anweisungen an, die für diese Methoden generiert werden. Im zweiten Fall fügte der JIT-Compiler eine zusätzliche cmp-Anweisung hinzu, die auf die Adresse des Objekts selbst zugreift, wodurch AccessViolation mit einer Nulladresse aufgerufen wird, die von der Laufzeit in eine NullReferenceException konvertiert wird.
Es ist erwähnenswert, dass es für dieses Experiment nicht ausreicht, ein Array als großes Objekt zu verwenden. Warum? Überlassen Sie diese Frage dem Leser, schreiben Sie Ideen in die Kommentare :)
Fassen wir die Experimente mit AccessViolation zusammen.

AccessViolationException verhält sich je nachdem, wo die Ausnahme aufgetreten ist (im verwalteten Code oder im nativen Code), unterschiedlich. Wenn im verwalteten Code eine Ausnahme aufgetreten ist, wird außerdem die Adresse des Objekts überprüft.
Die Frage ist: Können wir eine AccessViolationException behandeln, die im nativen Code oder im verwalteten Code aufgetreten ist, aber nicht in NullReference konvertiert und nicht mit throw ausgelöst wurde? Dies ist manchmal eine nützliche Funktion, insbesondere wenn Sie mit unsicherem Code arbeiten. Die Antwort auf diese Frage hängt von der Version von .NET ab.

In .NET 1.0 gab es überhaupt keine AccessViolationException. Alle Links wurden entweder als gültig oder als null angesehen. Zum Zeitpunkt von .NET 2.0 wurde klar, dass ohne direkte Arbeit mit dem Speicher - auf keinen Fall - AccessViolation angezeigt wurde, während es verarbeitbar war. In 4.0 und höher blieb es noch funktionsfähig, aber die Verarbeitung ist nicht so einfach. Um diese Ausnahme abzufangen, müssen Sie jetzt die Methode, in der sich der catch-Block befindet, mit dem HandleProcessCorruptedStateException-Attribut markieren. Anscheinend haben die Entwickler dies getan, weil sie der Meinung waren, dass AccessViolationException nicht die Ausnahme ist, die in einer regulären Anwendung abgefangen werden sollte.
Aus Gründen der Abwärtskompatibilität können außerdem die Laufzeiteinstellungen verwendet werden:
- LegacyNullReferenceExceptionPolicy gibt das .NET 1.0-Verhalten zurück - alle AVs werden zu NRE
- LegacyCorruptedStateExceptionsPolicy gibt das .NET 2.0-Verhalten zurück - alle AVs werden abgefangen
In .NET wird Core AccessViolation überhaupt nicht behandelt.
In unserer Produktion gab es eine solche Situation:

Eine unter .NET 4.7.1 erstellte Anwendung verwendete eine unter .NET 3.5 erstellte gemeinsam genutzte Codebibliothek. In dieser Bibliothek gab es einen Helfer, der eine regelmäßige Aktion ausführte:
while (isRunning) { try { action(); } catch (Exception e) { log.Error(e); } WaitForNextExecution(... ); }
Wir haben die Aktion aus unserer Bewerbung an diesen Helfer weitergeleitet. So kam es, dass er mit AccessViolation abstürzte. Infolgedessen protokollierte unsere Anwendung ständig AccessViolation, anstatt weil zu stürzen Der Code in der Bibliothek unter 3.5 könnte ihn abfangen. Es ist zu beachten, dass das Abfangen nicht von der Version der Laufzeit abhängt, auf der die Anwendung ausgeführt wird, sondern von TargetFramework, unter dem die Anwendung erstellt wurde, und ihren Abhängigkeiten.
Zusammenfassend. Die AccessVilolation-Verarbeitung hängt davon ab, woher sie stammt - in nativem oder verwaltetem Code - sowie von den TargetFramework- und Laufzeiteinstellungen.
Thread abbrechen
Manchmal müssen Sie im Code die Ausführung eines der Threads stoppen. Dazu können Sie den thread.Abort () verwenden.
var thread = new Thread(() => { try { ... } catch (ThreadAbortException e) { ... Thread.ResetAbort(); } }); ... thread.Abort();
Wenn die Abort-Methode in einem gestoppten Thread aufgerufen wird, wird eine ThreadAbortException ausgelöst. Lassen Sie uns seine Funktionen analysieren. Zum Beispiel ein Code wie dieser:
var thread = new Thread(() => { try { … } catch (ThreadAbortException e) { … } }); ... thread.Abort();
Absolut gleichbedeutend damit:
var thread = new Thread(() => { try { ... } catch (ThreadAbortException e) { ... throw; } }); ... thread.Abort();
Wenn Sie ThreadAbort noch verarbeiten und einige andere Aktionen im gestoppten Thread ausführen müssen, können Sie die Thread.ResetAbort () -Methode verwenden. Es stoppt den Prozess des Stoppens des Flusses und die Ausnahme hört auf, den Stapel höher zu werfen. Es ist wichtig zu verstehen, dass die thread.Abort () -Methode selbst nichts garantiert - der Code im gestoppten Thread verhindert möglicherweise das Stoppen.
Ein weiteres Merkmal von thread.Abort () ist, dass es den Code nicht unterbrechen kann, wenn er sich im catch befindet und schließlich blockiert.
Im Framework-Code finden Sie häufig Methoden, bei denen der try-Block leer ist und die gesamte Logik endgültig enthalten ist. Dies geschieht nur, um zu verhindern, dass dieser Code von einer ThreadAbortException ausgelöst wird.
Außerdem wartet ein Aufruf der thread.Abort () -Methode darauf, dass eine ThreadAbortException ausgelöst wird. Kombinieren Sie diese beiden Fakten und stellen Sie sicher, dass die thread.Abort () -Methode den aufrufenden Thread blockieren kann.
var thread = new Thread(() => { try { } catch { }
In der Realität kann dies bei der Verwendung der Verwendung auftreten. Es wird in try / finally bereitgestellt. Innerhalb von finally wird die Dispose-Methode aufgerufen. Es kann beliebig komplex sein, Ereignishandler enthalten und Sperren verwenden. Und wenn thread.Abort zur Laufzeit aufgerufen wurde, wartet Dispose - thread.Abort () darauf. So bekommen wir ein Schloss fast von Grund auf neu.
In .NET Core löst die thread.Abort () -Methode eine PlatformNotSupportedException aus. Und ich denke, das ist sehr gut, weil es mich motiviert, nicht thread.Abort (), sondern nicht-invasive Methoden zu verwenden, um die Codeausführung zu stoppen, beispielsweise mit dem CancellationToken.
AUS DEM SPEICHER
Diese Ausnahme kann erhalten werden, wenn der Speicher auf dem Computer geringer als erforderlich ist. Oder als wir auf die Einschränkungen eines 32-Bit-Prozesses stießen. Sie können es jedoch auch dann herunterladen, wenn der Computer über viel freien Speicher verfügt und der Prozess 64-Bit ist.
var arr4gb = new int[int.MaxValue/2];
Der obige Code löst OutOfMemory aus. Die Sache ist, dass Objekte mit mehr als 2 GB standardmäßig nicht zulässig sind. Dies kann behoben werden, indem gcAllowVeryLargeObjects in App.config festgelegt wird. In diesem Fall wird ein 4-GB-Array erstellt.
Versuchen wir nun, ein Array noch weiter zu erstellen.
var largeArr = new int[int.MaxValue];
Jetzt hilft auch gcAllowVeryLargeObjects nicht mehr. Dies liegt daran, dass .NET
den maximalen Index in einem Array begrenzt . Diese Einschränkung ist kleiner als int.MaxValue.
Maximaler Array-Index:
- Byte-Arrays - 0x7FFFFFC7
- andere Arrays - 0X7F E FFFFF
In diesem Fall tritt eine OutOfMemoryException auf, obwohl wir tatsächlich auf eine Datentypeinschränkung gestoßen sind, nicht auf einen Speichermangel.
Manchmal wird OutOfMemory durch verwalteten Code im .NET Framework explizit weggeworfen:

Dies ist eine Implementierung der string.Concat-Methode. Wenn die Länge der Ergebniszeichenfolge größer als int.MaxValue ist, wird sofort eine OutOfMemoryException ausgelöst.
Kommen wir zu der Situation, in der OutOfMemory auftritt, wenn der Speicher tatsächlich leer ist.
LimitMemory(64.Mb()); try { while (true) list.Add(new byte[size]); } catch (OutOfMemoryException e) { Console.WriteLine(e); }
Zunächst beschränken wir den Speicher unseres Prozesses auf 64 MB. Wählen Sie als Nächstes in der Schleife neue Byte-Arrays aus, speichern Sie sie auf einem Blatt, damit der GC sie nicht sammelt, und versuchen Sie, OutOfMemory abzufangen.
In diesem Fall kann alles passieren:
- Ausnahme behandelt
- Prozess wird fallen
- Lassen Sie uns in den Fang gehen, aber die Ausnahme wird wieder abstürzen
- Lassen Sie uns in den Fang gehen, aber StackOverflow wird abstürzen
In diesem Fall ist das Programm vollständig nicht deterministisch. Lassen Sie uns alle Optionen analysieren:
- Eine Ausnahme kann behandelt werden. In .NET hindert Sie nichts daran, eine OutOfMemoryException zu behandeln.
- Der Prozess kann fallen. Vergessen Sie nicht, dass wir eine verwaltete Anwendung haben. Dies bedeutet, dass darin nicht nur unser Code ausgeführt wird, sondern auch der Laufzeitcode. Zum Beispiel GC. Daher kann es vorkommen, dass die Laufzeit Speicher für sich selbst reservieren möchte, dies jedoch nicht kann. Dann können wir die Ausnahme nicht abfangen.
- Gehen wir in den Haken, aber die Ausnahme wird wieder abstürzen. Innerhalb von catch erledigen wir den Job auch dort, wo wir Speicher benötigen (wir drucken eine Ausnahme auf die Konsole), und dies kann eine neue Ausnahme verursachen.
- Lassen Sie uns in den Fang gehen, aber StackOverflow wird abstürzen. StackOverflow selbst tritt auf, wenn die WriteLine-Methode aufgerufen wird, aber es gibt hier keinen Stapelüberlauf, aber eine andere Situation tritt auf. Lassen Sie es uns genauer analysieren.

Im virtuellen Speicher können Seiten nicht nur dem physischen Speicher zugeordnet, sondern auch reserviert werden. Wenn die Seite reserviert ist, hat die Anwendung festgestellt, dass sie verwendet werden soll. Wenn die Seite bereits einem realen Speicher oder Swap zugeordnet ist, wird sie als "festgeschrieben" (festgeschrieben) bezeichnet. Der Stapel verwendet diese Fähigkeit, um Speicher in reservierte und festgeschriebene zu teilen. Es sieht ungefähr so aus:

Es stellt sich heraus, dass wir die WriteLine-Methode aufrufen, die einen Platz auf dem Stapel einnimmt. Es stellt sich heraus, dass der gesamte gesperrte Speicher bereits beendet ist. Dies bedeutet, dass das Betriebssystem zu diesem Zeitpunkt eine weitere reservierte Seite auf dem Stapel nehmen und sie dem realen physischen Speicher zuordnen sollte, der bereits mit Byte-Arrays gefüllt ist. Dies führt zur Ausnahme von StackOverflow.
Mit dem folgenden Code können Sie den gesamten Speicher zu Beginn des Streams auf einmal auf den Stapel übertragen.
new Thread(() => F(), 4*1024*1024).Start();
Alternativ können Sie die
Laufzeiteinstellung disableCommitThreadStack verwenden. Es muss deaktiviert werden, damit der Thread-Stack im Voraus festgeschrieben wird. Es ist anzumerken, dass das in der Dokumentation beschriebene und in der Realität beobachtete Standardverhalten unterschiedlich ist.

Stapelüberlauf
Schauen wir uns StackOverflowException genauer an. Schauen wir uns zwei Codebeispiele an. In einem von ihnen führen wir eine unendliche Rekursion aus, die zu einem Stapelüberlauf führt, in dem zweiten werfen wir diese Ausnahme einfach mit throw.
try { InfiniteRecursion(); } catch (Exception) { ... }
try { throw new StackOverflowException(); } catch (Exception) { ... }
Da alle mit throw ausgelösten Ausnahmen behandelt werden, werden wir im zweiten Fall die Ausnahme abfangen. Und beim ersten Fall ist alles interessanter. Wenden Sie sich an
MSDN :
"Sie können keine Stapelüberlauf-Ausnahmen abfangen, da für den Ausnahmebehandlungscode möglicherweise der Stapel erforderlich ist."
MSDN
Hier heißt es, dass wir keine StackOverflowException abfangen können, da das Abfangen selbst möglicherweise zusätzlichen Stapelspeicherplatz benötigt, der bereits beendet wurde.
Um uns irgendwie vor dieser Ausnahme zu schützen, können wir Folgendes tun. Zunächst können Sie die Tiefe der Rekursion begrenzen. Zweitens können Sie die Methoden der RuntimeHelpers-Klasse verwenden:
RuntimeHelpers.EnsureSufficientExecutionStack ();
- "Stellt sicher, dass der verbleibende Stapelspeicher groß genug ist, um die durchschnittliche .NET Framework-Funktion auszuführen." - MSDN
- UnzureichendeExecutionStackException
- 512 KB - x86, AnyCPU, 2 MB - x64 (Hälfte der Stapelgröße)
- 64/128 KB - .NET Core
- Überprüfen Sie nur den Stapeladressraum
In der Dokumentation zu dieser Methode wird überprüft, ob auf dem Stapel genügend Speicherplatz vorhanden ist, um die
durchschnittliche .NET-Funktion auszuführen. Aber was ist die
durchschnittliche Funktion? Tatsächlich überprüft diese Methode in .NET Framework, ob mindestens die Hälfte ihrer Größe auf dem Stapel frei ist. In .NET Core wird nach 64 KB kostenlos gesucht.
In .NET Core wurde auch ein Analogon angezeigt: RuntimeHelpers.TryEnsureSufficientExecutionStack (), das einen Bool zurückgibt, anstatt eine Ausnahme auszulösen.
Mit C # 7.2 wurde die Möglichkeit eingeführt, Span und Stackallock zusammen zu verwenden, ohne unsicheren Code zu verwenden. Möglicherweise wird Stackalloc aus diesem Grund häufiger im Code verwendet, und es ist hilfreich, sich bei der Verwendung vor StackOverflow zu schützen und auszuwählen, wo Speicher zugewiesen werden soll. Als solches Verfahren wird ein Verfahren vorgeschlagen
, das die Möglichkeit der Zuordnung auf dem Stapel und dem
Trystackalloc- Konstrukt
überprüft .
Span<byte> span; if (CanAllocateOnStack(size)) span = stackalloc byte[size]; else span = new byte[size];
Zurück zur StackOverflow-Dokumentation zu MSDN
Wenn stattdessen in einer normalen Anwendung ein Stapelüberlauf auftritt, beendet die Common Language Runtime (CLR) den Prozess. “
MSDN
Wenn es eine „normale“ Anwendung gibt, die während StackOverflow herunterfällt, gibt es nicht normale Anwendungen, die nicht fallen? Um diese Frage zu beantworten, müssen Sie eine Ebene von der Ebene der verwalteten Anwendung auf die Ebene der CLR senken.

"Eine Anwendung, die die CLR hostet, kann das Standardverhalten ändern und angeben, dass die CLR die Anwendungsdomäne entlädt, in der die Ausnahme auftritt, den Prozess jedoch fortsetzen kann." - MSDN
StackOverflowException -> AppDomainUnloadedException
Eine Anwendung, die die CLR hostet, kann das Verhalten des Stapelüberlaufs neu definieren, sodass anstelle des Abschlusses des gesamten Prozesses die Anwendungsdomäne entladen wird, in deren Stream dieser Überlauf aufgetreten ist. So können wir eine StackOverflowException in eine AppDomainUnloadedException verwandeln.
Wenn eine verwaltete Anwendung gestartet wird, wird die .NET-Laufzeit automatisch gestartet. Aber du kannst den anderen Weg gehen. Schreiben Sie beispielsweise eine nicht verwaltete Anwendung (in C ++ oder einer anderen Sprache), die eine spezielle API verwendet, um die CLR zu erhöhen und unsere Anwendung zu starten. Eine Anwendung, die die CLR intern ausführt, wird als CLR-Host bezeichnet. Durch das Schreiben können wir viele Dinge zur Laufzeit konfigurieren. Ersetzen Sie beispielsweise den Speichermanager und den Thread-Manager. Wir in der Produktion verwenden CLR-Host, um das Austauschen von Speicherseiten zu vermeiden.
Der folgende Code konfiguriert den CLR-Host so, dass AppDomain (C ++) während StackOverflow entladen wird:
ICLRPolicyManager *policyMgr; pCLRControl->GetCLRManager(IID_ICLRPolicyManager, (void**) (&policyMgr)); policyMgr->SetActionOnFailure(FAIL_StackOverflow, eRudeUnloadAppDomain);
Ist dies ein guter Weg, um StackOverflow zu entkommen? Wahrscheinlich nicht sehr. Erstens mussten wir C ++ - Code schreiben, was wir nicht wollten. Zweitens müssen wir unseren C # -Code so ändern, dass die Funktion, die eine StackOverflowException auslösen kann, in einer separaten AppDomain und in einem separaten Thread ausgeführt wird. Unser Code wird sofort zu solchen Nudeln:
try { var appDomain = AppDomain.CreateDomain("..."); appDomain.DoCallBack(() => { var thread = new Thread(() => InfiniteRecursion()); thread.Start(); thread.Join(); }); AppDomain.Unload(appDomain); } catch (AppDomainUnloadedException) { }
Um die InfiniteRecursion-Methode aufzurufen, haben wir eine Reihe von Zeilen geschrieben. Drittens haben wir begonnen, AppDomain zu verwenden. Und das garantiert fast eine Reihe neuer Probleme. Einschließlich mit Ausnahmen. Betrachten Sie ein Beispiel:
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)
Da unsere Ausnahme nicht als serialisierbar markiert ist, wird unser Code mit einer SerializationException gelöscht. Um dieses Problem zu beheben, reicht es nicht aus, unsere Ausnahme mit dem Attribut Serializable zu markieren. Wir müssen dennoch einen zusätzlichen Konstruktor für die Serialisierung implementieren.
[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());
Es stellt sich heraus, dass alles nicht sehr schön ist, also gehen wir weiter - auf die Ebene des Betriebssystems und der Hacks, die nicht in der Produktion verwendet werden sollten.
Seh / veh

Beachten Sie, dass während verwaltete Ausnahmen zwischen verwaltet und der CLR flogen, SEH-Ausnahmen zwischen der CLR und Windows flogen.
SEH - Strukturierte Ausnahmebehandlung
- Windows-Ausnahmebehandlungsmodul
- Einheitliche Behandlung von Software- und Hardware-Ausnahmen
- C # -Ausnahmen zusätzlich zu SEH implementiert
SEH ist ein Ausnahmebehandlungsmechanismus in Windows, mit dem Sie Ausnahmen, die beispielsweise von der Prozessorebene stammen oder mit der Logik der Anwendung selbst zusammenhängen, gleichermaßen einheitlich behandeln können.
Rantime .NET kennt SEH-Ausnahmen und kann sie in verwaltete Ausnahmen konvertieren:
- EXCEPTION_STACK_OVERFLOW -> Absturz
- EXCEPTION_ACCESS_VIOLATION -> AccessViolationException
- EXCEPTION_ACCESS_VIOLATION -> NullReferenceException
- EXCEPTION_INT_DIVIDE_BY_ZERO -> DivideByZeroException
- Unbekannte SEH-Ausnahmen -> SEHException
Wir können über WinApi mit SEH interagieren.
[DllImport("kernel32.dll")] static extern void RaiseException(uint dwExceptionCode, uint dwExceptionFlags, uint nNumberOfArguments,IntPtr lpArguments);
Tatsächlich funktioniert das Wurfkonstrukt auch über SEH. throw -> RaiseException(0xe0434f4d, ...)
Hierbei ist zu beachten, dass der CLR-Ausnahmecode immer derselbe ist. Unabhängig davon, welche Art von Ausnahme wir auslösen, wird er immer verarbeitet.VEH ist eine Vektorausnahmebehandlung, eine Erweiterung von SEH, die jedoch auf Prozessebene und nicht auf der Ebene eines einzelnen Threads arbeitet. Wenn SEH dem Try-Catch semantisch ähnlich ist, ist VEH einem Interrupt-Handler semantisch ähnlich. Wir stellen einfach unseren Handler ein und können Informationen über alle Ausnahmen erhalten, die in unserem Prozess auftreten. Eine interessante Funktion von VEH ist, dass Sie die SEH-Ausnahme ändern können, bevor sie zum Handler gelangt.
Wir können unseren eigenen Vektor-Handler zwischen das Betriebssystem und die Laufzeit stellen, der SEH-Ausnahmen behandelt. Wenn EXCEPTION_STACK_OVERFLOW auftritt, können Sie ihn so ändern, dass die .NET-Laufzeit den Prozess nicht zum Absturz bringt.Sie können über WinApi mit VEH interagieren: [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; ... }
Der Kontext enthält Informationen zum Status aller Prozessorregister zum Zeitpunkt der Ausnahme. Wir werden an EXCEPTION_RECORD und dem darin enthaltenen ExceptionCode-Feld interessiert sein. Wir können es durch unseren eigenen Ausnahmecode ersetzen, von dem die CLR nichts weiß. Der Vektor-Handler sieht folgendermaßen aus: 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; }
Jetzt erstellen wir einen Wrapper, der einen Vektorhandler in Form der HandleSO-Methode installiert, der einen Delegaten aufnimmt, der möglicherweise aus einer StackOverflowException stammt (aus Gründen der Übersichtlichkeit behandelt der Code keine WinApi-Funktionsfehler und entfernt den Vektorhandler). 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());
Darin wird auch die SetThreadStackGuarantee-Methode verwendet. Diese Methode reserviert Stapelspeicher für die StackOverflow-Verarbeitung.Auf diese Weise können wir den Aufruf einer Methode mit unendlicher Rekursion überleben. Unser Stream funktioniert weiterhin so, als wäre nichts passiert, als wäre kein Überlauf aufgetreten.Aber was passiert, wenn Sie HandleSO zweimal im selben Thread aufrufen? HandleSO(() => InfiniteRecursion()); HandleSO(() => InfiniteRecursion());
Und es wird eine AccessViolationException geben. Zurück zum Stapelgerät.
Das Betriebssystem kann Stapelüberläufe erkennen. Ganz oben im Stapel befindet sich eine spezielle Seite, die mit dem Guard-Seitenflag gekennzeichnet ist. Beim ersten Zugriff auf diese Seite tritt eine weitere Ausnahme auf - STATUS_GUARD_PAGE_VIOLATION - und das Flag für die Seitenwache wird von der Seite entfernt. Wenn Sie diesen Überlauf einfach abfangen, befindet sich diese Seite nicht mehr auf dem Stapel. Beim nächsten Überlauf kann das Betriebssystem dies nicht verstehen und der Stapelzeiger geht über den für den Stapel zugewiesenen Speicher hinaus. Infolgedessen tritt eine AccessViolationException auf. Sie müssen also Seitenflags nach der Verarbeitung von StackOverflow wiederherstellen. Der einfachste Weg, dies zu tun, ist die Verwendung der Methode _resetstkoflw aus der C-Laufzeitbibliothek (msvcrt.dll). [DllImport("msvcrt.dll")] static extern int _resetstkoflw();
Auf ähnliche Weise können Sie eine AccessViolationException in .NET Core unter Windows abfangen, wodurch der Prozess abstürzt. In diesem Fall müssen Sie die Reihenfolge berücksichtigen, in der Vektorhandler aufgerufen werden, und Ihren Handler auf den Anfang der Kette setzen, da .NET Core bei der Verarbeitung von AccessViolation auch VEH verwendet. Der erste Parameter der Funktion AddVectoredExceptionHandler ist für die Reihenfolge verantwortlich, in der die Handler aufgerufen werden: Kernel32.AddVectoredExceptionHandler(FirstHandler: (IntPtr) 1, handler);
Nachdem wir praktische Fragen untersucht haben, fassen wir die allgemeinen Ergebnisse zusammen:- Ausnahmen sind nicht so einfach, wie sie scheinen;
- Nicht alle Ausnahmen werden gleich behandelt.
- Die Ausnahmebehandlung erfolgt auf verschiedenen Abstraktionsebenen.
- Sie können in den Ausnahmebehandlungsprozess eingreifen und die .NET-Laufzeit anders als ursprünglich vorgesehen ausführen.
Referenzen
→ Repository mit Beispielen aus dem Bericht→ Dotnext 2016 Moskau - Adam Sitnik - Außergewöhnliche Ausnahmen in .NET→ DotNetBook: Ausnahmen→ .NET Inside Out Teil 8 - Behandlung von Stapelüberlauf-Ausnahmen in C # mit VEH ist eine weitere Möglichkeit, StackOverflow abzufangen.Am 22. und 23. November wird Eugene auf der DotNext 2018 in Moskau mit einem Bericht "System Metrics: Collecting Pitfalls" sprechen . Jeffrey Richter, Greg Young, Pavel Yosifovich und andere ebenso interessante Redner werden nach Moskau kommen. Die Themen der Berichte können hier eingesehen werden und Tickets können hier gekauft werden . Jetzt mitmachen!