c# - unauthorizedaccessexception - ¿Por qué.NET se comporta tan mal cuando se lanza StackOverflowException?
system.stackoverflowexception error in c# (3)
Soy consciente de que StackOverflowExceptions en .NET no se puede capturar, eliminar su proceso y no tener ningún rastro de pila. Esto está oficialmente documentado en MSDN . Sin embargo, me pregunto cuáles son las razones técnicas (u otras) detrás del comportamiento. Todo lo que MSDN dice es:
En versiones anteriores de .NET Framework, su aplicación podría capturar un objeto StackOverflowException (por ejemplo, para recuperarse de la recursión ilimitada). Sin embargo, esa práctica actualmente se desaconseja porque se requiere un código adicional significativo para capturar de manera confiable una excepción de desbordamiento de pila y continuar la ejecución del programa.
¿Qué es este "código adicional significativo"? ¿Hay otras razones documentadas para este comportamiento? Incluso si no podemos detectar SOE, ¿por qué no podemos obtener al menos un rastro de pila? Varios compañeros de trabajo y yo nos hundimos varias horas en la depuración de una producción StackOverflowException que habría llevado minutos con un seguimiento de pila, por lo que me pregunto si hay una buena razón para mi sufrimiento.
La pila de un hilo es creada por Windows. Utiliza las denominadas páginas de guardia para poder detectar un desbordamiento de pila. Una característica que generalmente está disponible para el código de modo de usuario como se describe en este artículo de MSDN Library . La idea básica es que las últimas dos páginas de la pila (2 x 4096 = 8192 bytes) están reservadas y cualquier acceso de procesador a ellas desencadena un error de página que se convierte en una excepción SEH, STATUS_GUARD_PAGE_VIOLATION.
Esto es interceptado por el núcleo en el caso de aquellas páginas que pertenecen a una pila de hilos. Cambia los atributos de protección de la primera de esas 2 páginas, lo que le da al subproceso cierto espacio de pila de emergencia para tratar el contratiempo, y luego vuelve a generar una excepción STATUS_STACK_OVERFLOW.
Esta excepción a su vez es interceptada por el CLR. En ese punto, quedan aproximadamente 3 kilobytes de espacio de pila. Esto, por un lado, no es suficiente para ejecutar el compilador Just-in-time (JITter) para compilar el código que podría tratar la excepción en su programa, el JITter necesita mucho más espacio que eso. Por lo tanto, el CLR no puede hacer otra cosa que abortar groseramente el hilo. Y por la política de .NET 2.0 que también finaliza el proceso.
Tenga en cuenta que este es un problema menor en Java, tiene un intérprete de código de bytes, por lo que existe la garantía de que se puede ejecutar el código de usuario ejecutable. O en un programa no administrado escrito en lenguajes como C, C ++ o Delphi, el código se genera en tiempo de compilación. Sin embargo, todavía es un percance muy difícil de manejar, el espacio de emergencia en la pila está volado, por lo que no hay ningún escenario en el que pueda continuar la ejecución del código en el hilo. La probabilidad de que un programa pueda continuar funcionando correctamente con un hilo anulado en una ubicación completamente aleatoria y en un estado bastante corrupto es bastante improbable.
Si hubo algún esfuerzo en considerar plantear un evento en otro hilo o eliminar la restricción en el winapi (el número de páginas de guardia no es configurable) entonces eso es un secreto muy bien guardado o simplemente no se consideró útil. Sospecho que el último, no lo sé por un hecho.
La pila es donde se almacena prácticamente todo sobre el estado de un programa. La dirección de cada sitio de retorno cuando se invocan métodos, variables locales, parámetros de método, etc. Si un método desborda la pila, su ejecución debe, por necesidad, detenerse inmediatamente (ya que no queda más espacio de pila para que continúe ejecutándose) . Luego, para recuperar con gracia, alguien tiene que limpiar todo lo que ese método hizo con la pila antes de que muriera. Esto significa saber cómo era la pila antes de llamar al método. Esto incurre en algunos gastos generales.
Y si no puede limpiar la pila, tampoco puede obtener un seguimiento de pila, porque la información requerida para generar la traza proviene de "desenrollar" la pila para descubrir qué métodos se invocaron.
Para manejar correctamente las condiciones de desbordamiento o falta de memoria de la pila, es necesario desencadenar una excepción antes de que la pila se haya desbordado o la memoria del montón esté totalmente agotada, en un momento en que los recursos de pila y pila disponibles serán adecuados para ejecutar cualquier código de limpieza que deberá ejecutarse antes de capturar las excepciones. En el caso de las excepciones de desbordamiento de pila, su manejo limpio básicamente requeriría verificar el puntero de la pila al ingresar a cada método (lo cual no debería ser tan costoso). Normalmente, se manejan configurando una trampa de violación de acceso justo después del final de la pila, pero el problema al hacer eso es que la trampa no se disparará hasta que ya sea demasiado tarde para manejar las cosas limpiamente. Uno podría configurar la trampa para disparar en el último bloque de memoria de la pila, en lugar de en el pasado, y hacer que el sistema cambie la trampa al bloque más allá de la pila una vez que se dispara y desencadena una Exception
, pero el problema es que habría no es una buena forma de asegurar que la trampa "casi fuera de pila" se vuelva a habilitar una vez que la pila se haya desenrollado tan lejos.
Habiendo dicho eso, un enfoque alternativo sería permitir que los hilos establezcan un delegado para lo que debería suceder si el hilo sopla su pila, y luego decir que en el caso de Exception
la pila del hilo se borrará y ejecutará el delegado suministrado. La trampa podría volverse a instalar antes de ejecutar el delegado (la pila estaría vacía en ese momento) y el código podría mantener un objeto de estado de subproceso que el delegado podría usar para saber si se omitió algún bloque importante finally
final.