c# - historia - ¿Por qué una llamada recursiva causa StackOverflow en diferentes niveles de pila?
stackoverflow historia (2)
Cambie r.Next()
a r.Next(10)
. StackOverflowException
s debe ocurrir en la misma profundidad.
Las cadenas generadas deberían consumir la misma memoria porque tienen el mismo tamaño. r.Next(10).ToString().Length == 1
siempre . r.Next().ToString().Length
es variable.
Lo mismo aplica si usa r.Next(100, 1000)
Estaba intentando descubrir cómo el compilador de C # maneja las llamadas de cola.
(Respuesta: No lo son, pero el JIT (s) de 64 bits hará TCE (eliminación de llamada de cola). Se aplican restricciones ).
Así que escribí una pequeña prueba usando una llamada recursiva que imprime cuántas veces se llama antes de que StackOverflowException
mate el proceso.
class Program
{
static void Main(string[] args)
{
Rec();
}
static int sz = 0;
static Random r = new Random();
static void Rec()
{
sz++;
//uncomment for faster, more imprecise runs
//if (sz % 100 == 0)
{
//some code to keep this method from being inlined
var zz = r.Next();
Console.Write("{0} Random: {1}/r", sz, zz);
}
//uncommenting this stops TCE from happening
//else
//{
// Console.Write("{0}/r", sz);
//}
Rec();
}
Justo a tiempo, el programa termina con SO Exception en cualquiera de:
- ''Optimizar compilación'' DESACTIVADO (ya sea Depuración o Versión)
- Objetivo: x86
- Objetivo: AnyCPU + "Prefer 32 bit" (esto es nuevo en VS 2012 y la primera vez que lo vi. Más aquí ).
- Alguna rama aparentemente inocua en el código (ver la rama comentada ''else'').
Por el contrario, usando ''Optimize build'' ON + (Target = x64 o AnyCPU con ''Prefer 32bit'' OFF (en una CPU de 64bit)), ocurre TCE y el contador sigue girando para siempre (vale, puede girar cada vez que se desborda su valor) )
Pero noté un comportamiento que no puedo explicar en el caso de StackOverflowException
: nunca (?) Ocurre exactamente en la misma profundidad de la pila. Aquí están los resultados de algunas ejecuciones de 32 bits, versión de lanzamiento:
51600 Random: 1778264579
Process is terminated due to StackOverflowException.
51599 Random: 1515673450
Process is terminated due to StackOverflowException.
51602 Random: 1567871768
Process is terminated due to StackOverflowException.
51535 Random: 2760045665
Process is terminated due to StackOverflowException.
Y construcción de depuración:
28641 Random: 4435795885
Process is terminated due to StackOverflowException.
28641 Random: 4873901326 //never say never
Process is terminated due to StackOverflowException.
28623 Random: 7255802746
Process is terminated due to StackOverflowException.
28669 Random: 1613806023
Process is terminated due to StackOverflowException.
El tamaño de la pila es constante (el valor predeterminado es 1 MB ). Los tamaños de los marcos de la pila son constantes.
Entonces, ¿qué puede explicar la variación (a veces no trivial) de la profundidad de la pila cuando golpea la StackOverflowException
?
ACTUALIZAR
Hans Passant plantea el problema de que Console.WriteLine
toque P / Invoke, interoperabilidad y posiblemente un bloqueo no determinista.
Así que simplifiqué el código a esto:
class Program
{
static void Main(string[] args)
{
Rec();
}
static int sz = 0;
static void Rec()
{
sz++;
Rec();
}
}
Lo ejecuté en Release / 32bit / Optimization ON sin un depurador. Cuando el programa falla, adjunto el depurador y verifico el valor del contador.
Y todavía no es lo mismo en varias carreras. (O mi prueba es defectuosa)
ACTUALIZACIÓN: cierre
Como lo sugirió fejesjoco, investigué ASLR (aleatorización de diseño de espacio de direcciones).
Es una técnica de seguridad que dificulta los ataques de desbordamiento de búfer para encontrar la ubicación precisa de (por ejemplo) llamadas al sistema específicas, aleatorizando varias cosas en el espacio de direcciones del proceso, incluida la posición de la pila y, aparentemente, su tamaño.
La teoría suena bien. ¡Pongamos en práctica!
Para probar esto, utilicé una herramienta de Microsoft específica para la tarea: EMET o The Enhanced Mitigation Experience Toolkit . Permite establecer el indicador ASLR (y mucho más) en un nivel de sistema o proceso.
(También hay una alternativa de pirateo de registro en todo el sistema que no probé)
Para verificar la efectividad de la herramienta, también descubrí que Process Explorer informa debidamente el estado del indicador ASLR en la página ''Propiedades'' del proceso. Nunca lo vi hasta hoy :)
Teóricamente, EMET puede (re) establecer el indicador ASLR para un solo proceso. En la práctica, no pareció cambiar nada (ver la imagen de arriba).
Sin embargo, desactivé ASLR para todo el sistema y (un reinicio más tarde) finalmente pude verificar que, de hecho, la excepción SO ahora siempre ocurre con la misma profundidad de pila.
PRIMA
Relacionado con ASLR, en noticias más antiguas: cómo consiguió Chrome pwned
Creo que puede ser ASLR en el trabajo. Puedes desactivar DEP para probar esta teoría.
Vea aquí una clase de utilidad C # para verificar la información de la memoria: https://.com/a/8716410/552139
Por cierto, con esta herramienta, encontré que la diferencia entre el tamaño máximo y mínimo de la pila es de alrededor de 2 KiB, que es media página. Eso es raro.
Actualización: OK, ahora sé que tengo razón. Seguí la teoría de media página y encontré este documento que examina la implementación de ASLR en Windows: http://www.symantec.com/avcenter/reference/Address_Space_Layout_Randomization.pdf
Citar:
Una vez que se ha colocado la pila, el puntero de pila inicial se aleatoriza adicionalmente mediante una cantidad decremental aleatoria. El desplazamiento inicial se selecciona para que sea de hasta media página (2,048 bytes)
Y esta es la respuesta a tu pregunta. ASLR elimina de forma aleatoria entre 0 y 2048 bytes de tu pila inicial.