Excepciones especiales en .NET y cómo prepararlas.

Varias excepciones en .NET tienen sus propias características, y conocerlas puede ser muy útil. ¿Cómo engañar al CLR? ¿Cómo mantenerse vivo en tiempo de ejecución capturando una excepción StackOverflowException? ¿Qué excepciones parece imposible atrapar, pero si realmente quieres, puedes?



Debajo del corte, la transcripción del informe de Eugene ( epeshk ) Peshkov de nuestra conferencia DotNext 2018 Piter , donde habló sobre estas y otras características de las excepciones.



Hola Me llamo Eugene Trabajo para SKB Kontur y desarrollo un sistema de alojamiento e implemento aplicaciones para Windows. La conclusión es que tenemos muchos equipos de productos que escriben sus propios servicios y los alojan con nosotros. Les brindamos una solución fácil y sencilla para una variedad de tareas de infraestructura. Por ejemplo, para monitorear el consumo de recursos del sistema o finalizar réplicas al servicio.

A veces resulta que las aplicaciones que están alojadas en nuestro sistema se desmoronan. Hemos visto muchas maneras en que una aplicación puede bloquearse en tiempo de ejecución. Uno de esos métodos es lanzar alguna excepción inesperada y encantadora.

Hoy hablaré sobre las características de las excepciones en .NET. Encontramos algunas de estas características en la producción, y algunas de ellas en el curso de los experimentos.

Plan


  1. Comportamiento de excepción .NET
  2. Manejo de excepciones de Windows y hacks

Todo lo siguiente es cierto para Windows. Todos los ejemplos se probaron en la última versión del framework completo .NET 4.7.1. También habrá algunas referencias a .NET Core.

Infracción de acceso


Esta excepción ocurre durante operaciones de memoria incorrectas. Por ejemplo, si una aplicación intenta acceder a un área de memoria a la que no tiene acceso. La excepción es de bajo nivel y, por lo general, si ocurre, se requerirá una depuración muy larga.

Intentemos obtener esta excepción usando C #. Para hacer esto, escribiremos el byte 42 a la dirección 1000 (suponemos que 1000 es una dirección bastante aleatoria y que nuestra aplicación probablemente no tiene acceso a ella).

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

WriteByte hace justo lo que necesitamos: escribe un byte en la dirección dada. Esperamos que esta llamada arroje una AccessViolationException. De hecho, este código arrojará esta excepción, podrá manejarlo y la aplicación continuará funcionando. Ahora cambiemos un poco el código:

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

Si en lugar de WriteByte usa el método Copy y copia el byte 42 a la dirección 1000, entonces usando try-catch, AccessViolation no puede ser capturado. Al mismo tiempo, se mostrará un mensaje en la consola que indica que la aplicación se finalizó debido a una AccessViolationException no controlada.

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

Resulta que tenemos dos líneas de código, mientras que la primera bloquea toda la aplicación con AccessViolation, y la segunda arroja una excepción procesada del mismo tipo. Para entender por qué sucede esto, veremos cómo se organizan estos métodos desde adentro.

Comencemos con el método Copiar.

 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); 

Lo único que hace el método Copy es llamar al método CopyToNative, implementado dentro de .NET. Si nuestra aplicación todavía falla y ocurre una excepción en algún lugar, esto solo puede ocurrir dentro de CopyToNative. Desde aquí podemos hacer la primera observación: si el código .NET llamado código nativo y AccessViolation se produjo dentro de él, entonces el código .NET no puede manejar esta excepción por alguna razón.

Ahora entenderemos por qué fue posible procesar AccessViolation usando el método WriteByte. Veamos el código para este método:

 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(); } } 

Este método está completamente implementado en código administrado. Utiliza el puntero C # para escribir datos en la dirección deseada y también captura una excepción NullReferenceException. Si se intercepta la NRE, se lanza una excepción AccessViolationException. Por lo tanto, es necesario debido a la especificación . En este caso, se manejan todas las excepciones lanzadas por la construcción de lanzamiento. En consecuencia, si se produce una NullReferenceException durante la ejecución del código dentro de WriteByte, podemos detectar AccessViolation. ¿Podría ocurrir una NRE, en nuestro caso, al acceder a la dirección 1000 en lugar de a la dirección cero?

Reescribimos el código usando punteros C # directamente, y vemos que cuando se accede a una dirección que no es cero, se produce una NullReferenceException:

 *(byte*) 1000 = 42; 

Para entender por qué sucede esto, debemos recordar cómo funciona la memoria del proceso. En la memoria del proceso, todas las direcciones son virtuales. Esto significa que la aplicación tiene un gran espacio de direcciones y solo algunas páginas se muestran en la memoria física real. Pero hay una peculiaridad: los primeros 64 KB de direcciones nunca se asignan a la memoria física y no se entregan a la aplicación. Rantime .NET lo sabe y lo usa. Si AccessViolation se produjo en el código administrado, el tiempo de ejecución verifica a qué dirección en la memoria se accedió y genera una excepción apropiada. Para direcciones de 0 a 2 ^ 16 - NullReference, para todos los demás - AccessViolation.



Veamos por qué se lanza la NullReference no solo cuando se accede a la dirección cero. Imagine que está accediendo a un campo de un objeto de un tipo de referencia, y la referencia a este objeto es nula:



En esta situación, esperamos obtener una NullReferenceException. El acceso al campo del objeto se produce en un desplazamiento relativo a la dirección de este objeto. Resulta que recurriremos a una dirección que esté lo suficientemente cerca de cero (recuerde que el enlace a nuestro objeto original es cero). Con este comportamiento de tiempo de ejecución, obtenemos la excepción esperada sin verificación adicional de la dirección del objeto en sí.

Pero, ¿qué sucede si pasamos al campo de un objeto y este objeto en sí ocupa más de 64 KB?



¿Podemos obtener AccessViolation en este caso? Hagamos un experimento. Creemos un objeto muy grande y nos referiremos a sus campos. Un campo al principio del objeto, el segundo al final:



Ambos métodos arrojarán una NullReferenceException. No se producirá AccessViolationException.
Veamos las instrucciones que se generarán para estos métodos. En el segundo caso, el compilador JIT agregó una instrucción cmp adicional que accede a la dirección del objeto en sí, llamando a AccessViolation con una dirección cero, que el tiempo de ejecución convertirá en una excepción NullReferenceException.

Vale la pena señalar que para este experimento no es suficiente usar una matriz como un objeto grande. Por qué Deje esta pregunta al lector, escriba ideas en los comentarios :)

Resumamos los experimentos con AccessViolation.



AccessViolationException se comporta de manera diferente dependiendo de dónde ocurrió la excepción (en código administrado o en nativo). Además, si se produjo una excepción en el código administrado, se verificará la dirección del objeto.

La pregunta es: ¿podemos manejar una AccessViolationException que ocurrió en el código nativo o en el código administrado, pero que no se convirtió a NullReference y no se lanzó mediante throw? Esta es a veces una característica útil, especialmente cuando se trabaja con código inseguro. La respuesta a esta pregunta depende de la versión de .NET.



En .NET 1.0, no había AccessViolationException en absoluto. Todos los enlaces se consideraron válidos o nulos. En el momento de .NET 2.0, quedó claro que sin trabajo directo con memoria, de ninguna manera, y apareció AccessViolation, mientras era procesable. En 4.0 y versiones posteriores, seguía siendo viable, pero procesarlo no es tan simple. Para detectar esta excepción, ahora debe marcar el método en el que se encuentra el bloque catch con el atributo HandleProcessCorruptedStateException. Aparentemente, los desarrolladores hicieron esto porque pensaron que AccessViolationException no era la excepción que debería quedar atrapada en una aplicación normal.
Además, para la compatibilidad con versiones anteriores, es posible utilizar la configuración de tiempo de ejecución:

  • legacyNullReferenceExceptionPolicy devuelve el comportamiento de .NET 1.0: todos los AV se convierten en NRE
  • legacyCorruptedStateExceptionsPolicy devuelve el comportamiento de .NET 2.0: todos los AV están interceptados

En .NET, Core AccessViolation no se maneja en absoluto.

En nuestra producción había tal situación:



Una aplicación creada bajo .NET 4.7.1 utilizó una biblioteca de código compartido construida bajo .NET 3.5. Había un ayudante en esta biblioteca para ejecutar una acción periódica:

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

Pasamos la acción de nuestra aplicación a este ayudante. Dio la casualidad de que se estrelló con AccessViolation. Como resultado, nuestra aplicación registró constantemente AccessViolation, en lugar de fallar porque el código en la biblioteca bajo 3.5 podría atraparlo. Cabe señalar que la intercepción no depende de la versión del tiempo de ejecución en el que se ejecuta la aplicación, sino del TargetFramework, bajo el cual se construyó la aplicación, y sus dependencias.

Para resumir. El procesamiento de AccessVilolation depende de dónde se originó, en código nativo o administrado, así como de TargetFramework y la configuración de tiempo de ejecución.

Hilo abortar


A veces, en el código, debe detener la ejecución de uno de los hilos. Para hacer esto, puede usar el thread.Abort ();

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

Cuando se llama al método Abort en un subproceso detenido, se genera una ThreadAbortException. Analicemos sus características. Por ejemplo, un código como este:

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

Absolutamente equivalente a esto:

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

Si aún necesita procesar ThreadAbort y realizar otras acciones en el hilo detenido, puede usar el método Thread.ResetAbort (); Detiene el proceso de detener el flujo y la excepción deja de arrojar más arriba en la pila. Es importante comprender que el método thread.Abort () en sí mismo no garantiza nada; el código en el hilo detenido puede evitar que se detenga.

Otra característica de thread.Abort () es que no podrá interrumpir el código si está en la captura y finalmente se bloquea.

Dentro del código marco, a menudo puede encontrar métodos en los que el bloque try está vacío y toda la lógica está finalmente dentro. Esto se hace solo para evitar que este código sea lanzado por una ThreadAbortException.

Además, una llamada al thread.Abort () espera a que se produzca una ThreadAbortException. Combine estos dos hechos y obtenga el hilo. El método Abort () puede bloquear el hilo que llama.

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

En realidad, esto se puede encontrar al usar el uso. Se implementa en try / finally, dentro de finalmente, se llama al método Dispose. Puede ser arbitrariamente complejo, contener controladores de eventos, usar bloqueos. Y si se llamó a thread.Abort en tiempo de ejecución, Dispose - thread.Abort () lo esperará. Entonces obtenemos un bloqueo casi desde cero.

En .NET Core, el método thread.Abort () produce una excepción PlatformNotSupportedException. Y creo que esto es muy bueno, porque me motiva a usar no thread.Abort (), sino métodos no invasivos para detener la ejecución de código, por ejemplo, usando CancellationToken.

FUERA DE MEMORIA


Esta excepción se puede obtener si la memoria en la máquina es menor que la requerida. O cuando nos encontramos con las limitaciones de un proceso de 32 bits. Pero puede obtenerlo incluso si la computadora tiene mucha memoria libre, y el proceso es de 64 bits.

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

El código anterior arrojará OutOfMemory. El problema es que, por defecto, los objetos de más de 2 GB no están permitidos. Esto se puede solucionar configurando gcAllowVeryLargeObjects en App.config. En este caso, se crea una matriz de 4 GB.

Ahora intentemos crear una matriz aún más.

 var largeArr = new int[int.MaxValue]; 

Ahora, incluso gcAllowVeryLargeObjects no ayudará. Esto se debe a que .NET tiene un límite en el índice máximo en una matriz . Esta restricción es menor que int.MaxValue.

Índice de matriz máx .:

  • conjuntos de bytes - 0x7FFFFFC7
  • otras matrices - 0X7F E FFFFF

En este caso, se producirá una excepción OutOfMemoryException, aunque de hecho hemos encontrado una restricción de tipo de datos, no una falta de memoria.

A veces, OutOfMemory es explícitamente descartado por el código administrado dentro del marco .NET:


Esta es una implementación del método string.Concat. Si la longitud de la cadena de resultado es mayor que int.MaxValue, se emite inmediatamente una OutOfMemoryException.

Pasemos a la situación en la que surge OutOfMemory en el caso en que la memoria realmente se agota.

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

Primero, limitamos la memoria de nuestro proceso a 64 MB. A continuación, dentro del bucle, seleccione nuevas matrices de bytes, guárdelas en alguna hoja para que el GC no las recopile e intente capturar OutOfMemory.

En este caso, cualquier cosa puede suceder:

  • Excepción manejada
  • El proceso caerá
  • Vamos a atrapar, pero la excepción se bloqueará nuevamente
  • Vamos a atrapar, pero StackOverflow se bloqueará

En este caso, el programa será completamente no determinista. Analicemos todas las opciones:

  1. Se puede manejar una excepción. Dentro de .NET, no hay nada que le impida manejar una excepción OutOfMemoryException.
  2. El proceso puede caer. No olvides que tenemos una aplicación administrada. Esto significa que en su interior se ejecuta no solo nuestro código, sino también el código de tiempo de ejecución. Por ejemplo, GC. Por lo tanto, una situación puede ocurrir cuando el tiempo de ejecución quiere asignar memoria para sí mismo, pero no puede hacerlo, entonces no podremos detectar la excepción.
  3. Vayamos a la captura, pero la excepción se bloqueará nuevamente. Dentro de catch, también hacemos el trabajo donde necesitamos memoria (imprimimos una excepción a la consola), y esto puede causar una nueva excepción.
  4. Vamos a atrapar, pero StackOverflow se bloqueará. StackOverflow en sí ocurre cuando se llama al método WriteLine, pero no hay desbordamiento de pila aquí, pero ocurre una situación diferente. Analicémoslo con más detalle.



En la memoria virtual, las páginas no solo se pueden asignar a la memoria física, sino que también se pueden reservar. Si la página está reservada, la aplicación señaló que la iba a usar. Si la página ya está asignada a memoria real o intercambio, entonces se llama "comprometida" (comprometida). La pila utiliza esta capacidad para dividir la memoria en reservada y comprometida. Se parece a esto:



Resulta que llamamos al método WriteLine, que ocupa un lugar en la pila. Resulta que toda la memoria comprometida ya ha finalizado, lo que significa que el sistema operativo en este momento debería tomar otra página reservada en la pila y asignarla a la memoria física real, que ya está llena de conjuntos de bytes. Esto lleva a la excepción de StackOverflow.

El siguiente código le permitirá enviar toda la memoria a la pila al comienzo de la secuencia a la vez.

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

Alternativamente, puede usar la configuración de tiempo de ejecución disableCommitThreadStack. Debe deshabilitarse para que la pila de subprocesos se confirme de antemano. Vale la pena señalar que el comportamiento predeterminado descrito en la documentación y observado en la realidad es diferente.



Desbordamiento de pila


Echemos un vistazo más de cerca a StackOverflowException. Veamos dos ejemplos de código. En uno de ellos, ejecutamos una recursión infinita, lo que conduce a un desbordamiento de la pila, en el segundo simplemente lanzamos esta excepción con throw.

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

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

Como todas las excepciones lanzadas con lanzamiento se manejan, en el segundo caso capturaremos la excepción. Y con el primer caso, todo es más interesante. Gire a MSDN :

"No se pueden detectar excepciones de desbordamiento de pila, porque el código de manejo de excepciones puede requerir la pila".
MSDN

Aquí dice que no podremos capturar una StackOverflowException, ya que la intercepción en sí misma puede requerir espacio de pila adicional que ya ha finalizado.

Para proteger de alguna manera contra esta excepción, podemos hacer lo siguiente. Primero, puede limitar la profundidad de la recursión. En segundo lugar, puede usar los métodos de la clase RuntimeHelpers:

RuntimeHelpers.EnsureSufficientExecutionStack ();

  • "Asegura que el espacio de pila restante sea lo suficientemente grande como para ejecutar la función promedio de .NET Framework". - MSDN
  • InsufficientExecutionStackException
  • 512 KB - x86, AnyCPU, 2 MB - x64 (la mitad del tamaño de la pila)
  • 64/128 KB - .NET Core
  • Comprobar solo el espacio de direcciones de la pila


La documentación de este método dice que verifica que haya suficiente espacio en la pila para ejecutar la función .NET promedio . Pero, ¿cuál es la función promedio ? De hecho, en .NET Framework este método verifica que al menos la mitad de su tamaño esté libre en la pila. En .NET Core, busca 64K gratis.

También ha aparecido un análogo en .NET Core: RuntimeHelpers. TryEnsureSufficientExecutionStack () que devuelve un bool, en lugar de lanzar una excepción.

C # 7.2 introdujo la capacidad de usar Span y stackallock juntos sin usar código inseguro. Quizás debido a esto, stackalloc se usará con más frecuencia en el código y será útil tener una forma de protegerse de StackOverflow cuando lo use, eligiendo dónde asignar memoria. Como tal método, se propone un método que verifica la posibilidad de asignación en la pila y la construcción trystackalloc .

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

Volver a la documentación de StackOverflow en MSDN

En cambio, cuando se produce un desbordamiento de pila en una aplicación normal , Common Language Runtime (CLR) finaliza el proceso ".
MSDN

Si hay una aplicación "normal" que cae durante StackOverflow, ¿hay aplicaciones no normales que no caen? Para responder a esta pregunta, deberá bajar un nivel desde el nivel de la aplicación administrada hasta el nivel CLR.



"Una aplicación que aloja el CLR puede cambiar el comportamiento predeterminado y especificar que el CLR descargue el dominio de la aplicación donde se produce la excepción, pero permite que el proceso continúe". - MSDN
StackOverflowException -> AppDomainUnloadedException

Una aplicación que aloja el CLR puede redefinir el comportamiento del desbordamiento de la pila para que, en lugar de completar todo el proceso, el dominio de la aplicación se descargue, en la secuencia en la que se produjo este desbordamiento. Entonces podemos convertir una StackOverflowException en una AppDomainUnloadedException.

Cuando se inicia una aplicación administrada, el tiempo de ejecución de .NET se inicia automáticamente. Pero puedes ir por el otro lado. Por ejemplo, escriba una aplicación no administrada (en C ++ u otro lenguaje) que utilizará una API especial para elevar el CLR e iniciar nuestra aplicación. Una aplicación que ejecuta el CLR internamente se llamará CLR-host. Al escribirlo, podemos configurar muchas cosas en tiempo de ejecución. Por ejemplo, reemplace el administrador de memoria y el administrador de hilos. En producción usamos CLR-host para evitar intercambiar páginas de memoria.

El siguiente código configura el CLR-host para que AppDomain (C ++) se descargue durante StackOverflow:

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

¿Es esta una buena manera de escapar de StackOverflow? Probablemente no muy. En primer lugar, teníamos que escribir código C ++, que no querríamos hacer. En segundo lugar, debemos cambiar nuestro código C # para que la función que puede lanzar una StackOverflowException se ejecute en un AppDomain separado y en un hilo separado. Nuestro código se convertirá inmediatamente en tales fideos:

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

Para llamar al método InfiniteRecursion, escribimos un montón de líneas. Tercero, comenzamos a usar AppDomain. Y esto casi garantiza un montón de nuevos problemas. Incluyendo con excepciones. Considere un ejemplo:

 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) 

Como nuestra excepción no está marcada como serializable, nuestro código caerá con una SerializationException. Y para solucionar este problema, no es suficiente para nosotros marcar nuestra excepción con el atributo Serializable, aún necesitamos implementar un constructor adicional para la serialización.

 [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()); 

Todo resulta no muy hermoso, por lo que vamos más allá: al nivel del sistema operativo y los hacks, que no deberían usarse en la producción.

Seh / veh




Tenga en cuenta que si bien las excepciones administradas volaron entre administrado y CLR, las excepciones SEH vuelan entre CLR y Windows.

SEH - Manejo de excepciones estructuradas

  • Motor de manejo de excepciones de Windows
  • Manejo uniforme de excepciones de software y hardware
  • Excepciones de C # implementadas sobre SEH

SEH es un mecanismo de manejo de excepciones en Windows, le permite manejar de manera uniforme todas las excepciones que vinieron, por ejemplo, desde el nivel del procesador, o que se asociaron con la lógica de la aplicación misma.

Rantime .NET conoce las excepciones SEH y puede convertirlas en excepciones administradas:

  • EXCEPTION_STACK_OVERFLOW -> Crash
  • EXCEPTION_ACCESS_VIOLATION -> AccessViolationException
  • EXCEPTION_ACCESS_VIOLATION -> NullReferenceException
  • EXCEPTION_INT_DIVIDE_BY_ZERO -> DivideByZeroException
  • Excepciones SEH desconocidas -> SEHException

Podemos interactuar con SEH a través de 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); 

De hecho, la construcción de lanzamiento también funciona a través de SEH.

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

Vale la pena señalar aquí que el código de excepción CLR es siempre el mismo, por lo que no importa qué tipo de excepción arrojemos, siempre se procesará.

VEH es un manejo de excepciones de vectores, una extensión de SEH, pero que funciona a nivel de proceso y no a nivel de un solo subproceso. Si SEH es semánticamente similar a try-catch, entonces VEH es semánticamente similar a un controlador de interrupciones. Simplemente configuramos nuestro controlador y podemos recibir información sobre todas las excepciones que ocurren en nuestro proceso. Una característica interesante de VEH es que le permite cambiar la excepción SEH antes de que llegue al controlador.



, 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; ... } 

Context . 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. .


. , Guard page. – STATUS_GUARD_PAGE_VIOLATION, Guard page . , – stack-pointer , . — AccessViolationException. StackOverflow – c – _resetstkoflw C (msvcrt.dll).

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

De manera similar, puede detectar una excepción AccessViolationException en .NET Core en Windows, lo que hace que el proceso se bloquee. En este caso, debe tener en cuenta el orden en que se llaman los manejadores de vectores y establecer su manejador al comienzo de la cadena, ya que .NET Core también usa VEH al procesar AccessViolation. El primer parámetro de la función AddVectoredExceptionHandler es responsable del orden en que se llaman los controladores:

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

Después de estudiar cuestiones prácticas, resumimos los resultados generales:

  • Las excepciones no son tan simples como parecen;
  • No todas las excepciones se manejan de la misma manera;
  • El manejo de excepciones ocurre en diferentes niveles de abstracción;
  • Puede intervenir en el proceso de manejo de excepciones y hacer que el tiempo de ejecución de .NET funcione de manera diferente a la prevista originalmente.

Referencias



Dotnext 2016 Moscow — Adam Sitnik — Exceptional Exceptions in .NET
DotNetBook: Exceptions
.NET Inside Out Part 8 — Handling Stack Overflow Exception in C# with VEH — StackOverflow.

22-23 DotNext 2018 Moscow « : » . , , . , — . !

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


All Articles