Desbordamiento de pila

Tipo de error de software

En software, se produce un desbordamiento de pila si el puntero de la pila de llamadas excede el límite de la pila . La pila de llamadas puede constar de una cantidad limitada de espacio de direcciones , a menudo determinada al comienzo del programa. El tamaño de la pila de llamadas depende de muchos factores, incluidos el lenguaje de programación, la arquitectura de la máquina, el multihilo y la cantidad de memoria disponible. Cuando un programa intenta utilizar más espacio del que está disponible en la pila de llamadas (es decir, cuando intenta acceder a la memoria más allá de los límites de la pila de llamadas, lo que es esencialmente un desbordamiento de búfer ), se dice que la pila se desborda , lo que generalmente da como resultado un bloqueo del programa . [1]

Causas

Recursión infinita

La causa más común de desbordamiento de pila es una recursión excesivamente profunda o infinita, en la que una función se llama a sí misma tantas veces que el espacio necesario para almacenar las variables y la información asociada con cada llamada es mayor que el que cabe en la pila. [2]

Un ejemplo de recursión infinita en C.

int foo () { devolver foo (); }    

La función foo , cuando se invoca, continúa invocándose a sí misma, asignando espacio adicional en la pila cada vez, hasta que la pila se desborda, lo que da como resultado un error de segmentación . [2] Sin embargo, algunos compiladores implementan la optimización de llamadas de cola , lo que permite que se produzca una recursión infinita de un tipo específico ( recursión de cola ) sin desbordamiento de pila. Esto funciona porque las llamadas de recursión de cola no ocupan espacio de pila adicional. [3]

Algunas opciones del compilador de C habilitarán efectivamente la optimización de la recursión de cola ; por ejemplo, compilar el programa simple anterior usando gcc con -O1resultará en una falla de segmentación, pero no cuando se usa -O2o -O3, ya que estos niveles de optimización implican la -foptimize-sibling-callsopción del compilador. [4] Otros lenguajes, como Scheme , requieren que todas las implementaciones incluyan la recursión de cola como parte del estándar del lenguaje. [5]

Recursión muy profunda

Una función recursiva que termina en teoría pero que provoca un desbordamiento del búfer de la pila de llamadas en la práctica se puede solucionar transformando la recursión en un bucle y almacenando los argumentos de la función en una pila explícita (en lugar de utilizar implícitamente la pila de llamadas). Esto siempre es posible porque la clase de funciones recursivas primitivas es equivalente a la clase de funciones computables LOOP. Considere este ejemplo en pseudocódigo similar a C++ :

void función ( argumento ) { if ( condición ) función ( argumento . siguiente );       }
pila . push ( argumento ); mientras ( ! pila . empty ()) { argumento = pila . pop (); si ( condición ) pila . push ( argumento . next ); }       

Una función recursiva primitiva como la del lado izquierdo siempre se puede transformar en un bucle como la del lado derecho.

Una función como la del ejemplo anterior a la izquierda no sería un problema en un entorno que admita la optimización de llamadas finales ; sin embargo, aún es posible crear una función recursiva que pueda generar un desbordamiento de pila en estos lenguajes. Considere el siguiente ejemplo de dos funciones de exponenciación de números enteros simples.

int pow ( int base , int exp ) { si ( exp > 0 ) devuelve base * pow ( base , exp - 1 ); de lo contrario devuelve 1 ; }                   
int pow ( int base , int exp ) { return pow_accum ( base , exp , 1 ); }         int pow_accum ( int base , int exp , int accum ) { si ( exp > 0 ) devuelve pow_accum ( base , exp - 1 , accum * base ); en caso contrario devolver acumulado ; }                      

Ambas pow(base, exp)funciones anteriores calculan un resultado equivalente; sin embargo, la de la izquierda es propensa a provocar un desbordamiento de pila porque no es posible optimizar las llamadas finales para esta función. Durante la ejecución, la pila de estas funciones se verá así:

poder ( 5 , 4 ) 5 * poder ( 5 , 3 ) 5 * ( 5 * poder ( 5 , 2 )) 5 * ( 5 * ( 5 * poder ( 5 , 1 ))) 5 * ( 5 * ( 5 * ( 5 * poder ( 5 , 0 )))) 5 * ( 5 * ( 5 * ( 5 * 1 ))) 625                                 
pow ( 5 , 4 ) pow_accum ( 5 , 4 , 1 ) pow_accum ( 5 , 3 , 5 ) pow_accum ( 5 , 2 , 25 ) pow_accum ( 5 , 1 , 125 ) pow_accum ( 5 , 0 , 625 ) 625           

Tenga en cuenta que la función de la izquierda debe almacenar en su pila expuna cantidad de números enteros, que se multiplicarán cuando la recursión finalice y la función devuelva 1. En cambio, la función de la derecha solo debe almacenar 3 números enteros a la vez y calcula un resultado intermedio que se pasa a su siguiente invocación. Como no se debe almacenar ninguna otra información fuera de la invocación de la función actual, un optimizador de recursión de cola puede "eliminar" los marcos de pila anteriores, eliminando así la posibilidad de un desbordamiento de pila.

Variables de pila muy grandes

La otra causa principal de un desbordamiento de pila es el intento de asignar más memoria en la pila de la que cabe, por ejemplo, creando variables de matriz locales que son demasiado grandes. Por este motivo, algunos autores recomiendan que las matrices más grandes que unos pocos kilobytes se asignen dinámicamente en lugar de como una variable local. [6]

Un ejemplo de una variable de pila muy grande en C :

int foo () { doble x [ 1048576 ]; }    

En una implementación de C con flotantes de doble precisión de 8 bytes , la matriz declarada consume 8 megabytes de datos; si esta es más memoria que la disponible en la pila (según lo establecido por los parámetros de creación de subprocesos o los límites del sistema operativo), se producirá un desbordamiento de pila.

Entorno restringido

Los desbordamientos de pila se agravan con cualquier cosa que reduzca el tamaño efectivo de pila de un programa determinado. Por ejemplo, el mismo programa que se ejecuta sin múltiples subprocesos puede funcionar bien, pero tan pronto como se habilita la multiprocesación, el programa se bloquea. Esto se debe a que la mayoría de los programas con subprocesos tienen menos espacio de pila por subproceso que un programa sin soporte de subprocesos. Debido a que los núcleos son generalmente multiproceso, a las personas que se inician en el desarrollo de núcleos generalmente se les desaconseja utilizar algoritmos recursivos o grandes búferes de pila. [7]

Véase también

Referencias

  1. ^ Burley, James Craig (1 de junio de 1991). "Uso y adaptación de GNU Fortran". Archivado desde el original el 6 de febrero de 2012.
  2. ^ ab ¿ Cuál es la diferencia entre una falla de segmentación y un desbordamiento de pila? Archivado el 13 de septiembre de 2021 en Wayback Machine en Stack Overflow
  3. ^ "Introducción al esquema y su implementación". 19 de febrero de 1997. Archivado desde el original el 10 de agosto de 2007.
  4. ^ "Uso de la Colección de compiladores GNU (GCC): Opciones de optimización". Archivado desde el original el 20 de agosto de 2017. Consultado el 20 de agosto de 2017 .
  5. ^ Richard Kelsey; William Clinger; Jonathan Rees; et al. (agosto de 1998). "Informe revisado5 sobre el esquema de lenguaje algorítmico". Computación simbólica y de orden superior . 11 (1): 7–105. doi :10.1023/A:1010051815785. S2CID  14069423. Archivado desde el original el 5 de enero de 2007. Consultado el 9 de agosto de 2012 .
  6. ^ Feldman, Howard (23 de noviembre de 2005). «Gestión moderna de la memoria, parte 2». Archivado desde el original el 20 de septiembre de 2012. Consultado el 14 de agosto de 2007 .
  7. ^ "Guía de programación del kernel: consejos sobre rendimiento y estabilidad". Apple Inc. 2 de mayo de 2014. Archivado desde el original el 3 de mayo de 2014. Consultado el 2 de mayo de 2014 .
  • Las razones por las que los programas de 64 bits requieren más memoria de pila Archivado el 4 de noviembre de 2017 en Wayback Machine
Retrieved from "https://en.wikipedia.org/w/index.php?title=Stack_overflow&oldid=1231191487"