¿Por qué el uso de alloca () no se considera una buena práctica?

401

alloca()asigna memoria en la pila en lugar de en el montón, como en el caso de malloc(). Entonces, cuando regreso de la rutina, la memoria se libera. Entonces, en realidad esto resuelve mi problema de liberar memoria asignada dinámicamente. La liberación de la memoria asignada malloc()es un gran dolor de cabeza y, si de alguna manera se pierde, conduce a todo tipo de problemas de memoria.

¿Por qué se alloca()desaconseja el uso a pesar de las características anteriores?

Vaibhav
fuente
40
Solo una nota rapida. Aunque esta función se puede encontrar en la mayoría de los compiladores, el estándar ANSI-C no la requiere y, por lo tanto, podría limitar la portabilidad. ¡Otra cosa es que no debes! free () el puntero que obtienes y se libera automáticamente después de salir de la función.
merkuro
99
Además, una función con alloca () no estará en línea si se declara como tal.
Justicle
2
@Justicle, ¿puedes dar alguna explicación? Tengo mucha curiosidad por saber qué hay detrás de este comportamiento
migajek
47
Olvídese de todo el ruido acerca de la portabilidad, no es necesario llamar free(lo que obviamente es una ventaja), no poder alinearlo (obviamente, las asignaciones del montón son mucho más pesadas), etc. La única razón para evitarlo allocaes para tamaños grandes. Es decir, desperdiciar toneladas de memoria de pila no es una buena idea, además tiene la posibilidad de un desbordamiento de pila. Si este es el caso, considere usar malloca/freea
valdo el
55
Otro aspecto positivo de allocaes que la pila no se puede fragmentar como el montón. Esto podría resultar útil para aplicaciones duras de estilo de ejecución permanente en tiempo real, o incluso aplicaciones críticas para la seguridad, ya que la WCRU puede analizarse estáticamente sin recurrir a agrupaciones de memoria personalizadas con su propio conjunto de problemas (sin localidad temporal, recurso subóptimo) utilizar).
Andreas

Respuestas:

245

La respuesta está ahí en la manpágina (al menos en Linux ):

VALOR DEVUELTO La función alloca () devuelve un puntero al comienzo del espacio asignado. Si la asignación provoca un desbordamiento de la pila, el comportamiento del programa no está definido.

Lo que no quiere decir que nunca debe usarse. Uno de los proyectos de OSS en los que trabajo lo usa ampliamente, y siempre y cuando no esté abusando de él ( allocacon grandes valores), está bien. Una vez que pasa la marca de "unos cientos de bytes", es hora de usar mallocy amigos, en su lugar. Es posible que aún obtenga fallas de asignación, pero al menos tendrá alguna indicación de la falla en lugar de simplemente volar la pila.

Sean Bright
fuente
35
Entonces, ¿realmente no hay problema con eso que no tendría también con declarar matrices grandes?
TED
88
@Sean: Sí, el riesgo de desbordamiento de pila es la razón dada, pero esa razón es un poco tonta. En primer lugar porque (como dice Vaibhav) las grandes matrices locales causan exactamente el mismo problema, pero no son tan vilipendiadas. Además, la recursión puede volar fácilmente la pila. Lo siento, pero te estoy pidiendo que contrarrestes la idea predominante de que la razón dada en la página del manual está justificada.
j_random_hacker
49
Mi punto es que la justificación dada en la página del manual no tiene sentido, ya que alloca () es exactamente tan "malo" como esas otras cosas (matrices locales o funciones recursivas) que se consideran kosher.
j_random_hacker
39
@ninjalj: No por programadores de C / C ++ altamente experimentados, pero creo que muchas personas que temen alloca()no tienen el mismo miedo a los arreglos locales o la recursión (de hecho, muchas personas que gritarán elogiarán la alloca()recursión porque "se ve elegante") . Estoy de acuerdo con el consejo de Shaun ("alloca () está bien para asignaciones pequeñas"), pero no estoy de acuerdo con la mentalidad de que enmarca a alloca () como un mal único entre los 3: ¡son igualmente peligrosos!
j_random_hacker
35
Nota: Dada la estrategia de asignación de memoria "optimista" de Linux, es muy probable que no obtenga ninguna indicación de un fallo de agotamiento del montón ... en su lugar, malloc () le devolverá un buen puntero no NULL, y luego cuando intente realmente Acceda al espacio de direcciones al que apunta, su proceso (o algún otro proceso, de forma impredecible) será asesinado por el asesino OOM. Por supuesto, esta es una "característica" de Linux en lugar de un problema de C / C ++ per se, pero es algo a tener en cuenta al debatir si alloca () o malloc () es "más seguro". :)
Jeremy Friesner
209

Uno de los errores más memorables que tuve fue hacer con una función en línea que usé alloca. Se manifestó como un desbordamiento de la pila (porque se asigna en la pila) en puntos aleatorios de la ejecución del programa.

En el archivo de encabezado:

void DoSomething() {
   wchar_t* pStr = alloca(100);
   //......
}

En el archivo de implementación:

void Process() {
   for (i = 0; i < 1000000; i++) {
     DoSomething();
   }
}

Entonces, lo que sucedió fue la DoSomethingfunción en línea del compilador y todas las asignaciones de pila ocurrían dentro de la Process()función y, por lo tanto, explotaban la pila. En mi defensa (y no fui yo quien descubrió el problema; tuve que ir a llorar a uno de los desarrolladores principales cuando no pude solucionarlo), no fue directo alloca, fue una conversión de cadenas ATL macros

Entonces, la lección es: no utilizar allocaen funciones que cree que podrían estar en línea.

Igor Zevaka
fuente
9191
Interesante. ¿Pero eso no calificaría como un error del compilador? Después de todo, la alineación cambió el comportamiento del código (retrasó la liberación del espacio asignado usando alloca).
sleske
6060
Aparentemente, al menos GCC tendrá esto en cuenta: "Tenga en cuenta que ciertos usos en una definición de función pueden hacer que no sea adecuado para la sustitución en línea. Entre estos usos están: uso de varargs, uso de alloca, [...]". gcc.gnu.org/onlinedocs/gcc/Inline.html
sleske
139
¿Qué compilador estabas fumando?
Thomas Eding
22
Lo que no entiendo es por qué el compilador no hace un buen uso del alcance para determinar que las alocas en el subscopio están más o menos "liberadas": el puntero de la pila podría volver a su punto antes de ingresar al alcance, como lo que se hace cuando regresando de la función (¿no?)
moala
8
He votado en contra, pero la respuesta está bien escrita: estoy de acuerdo con otros en los que estás criticando a alloca por lo que claramente es un error del compilador . El compilador ha hecho una suposición defectuosa en una optimización que no debería haber hecho. Trabajar en torno a un error del compilador está bien, pero no criticaría nada más que el compilador.
Evan Carroll
75

Antigua pregunta, pero nadie mencionó que debería reemplazarse por matrices de longitud variable.

char arr[size];

en vez de

char *arr=alloca(size);

Está en el estándar C99 y existió como extensión del compilador en muchos compiladores.

Patrick Schlüter
fuente
55
Jonathan Leffler lo menciona en un comentario a la respuesta de Arthur Ulfeldt.
ninjalj
2
De hecho, pero también muestra lo fácil que se pierde, ya que no lo había visto a pesar de leer todas las respuestas antes de publicar.
Patrick Schlüter
66
Una nota: son matrices de longitud variable, no matrices dinámicas. Estos últimos son redimensionables y generalmente se implementan en el montón.
Tim Čas
1
Visual Studio 2015 compilando algunos C ++ tiene el mismo problema.
ahcox
2
A Linus Torvalds no le gustan los VLA en el kernel de Linux . A partir de la versión 4.20, Linux debería estar casi libre de VLA.
Cristian Ciupitu
60

alloca () es muy útil si no puede usar una variable local estándar porque su tamaño debería determinarse en tiempo de ejecución y puede garantizar absolutamente que el puntero que obtiene de alloca () NUNCA se usará después de que esta función regrese .

Puedes estar bastante seguro si

  • no devuelva el puntero ni nada que lo contenga.
  • no almacene el puntero en ninguna estructura asignada en el montón
  • no permita que ningún otro hilo use el puntero

El peligro real proviene de la posibilidad de que alguien más viole estas condiciones en algún momento posterior. Con eso en mente, es ideal para pasar buffers a funciones que les dan formato de texto :)

Arthur Ulfeldt
fuente
12
La función VLA (matriz de longitud variable) de C99 admite variables locales de tamaño dinámico sin requerir explícitamente que se use alloca ().
Jonathan Leffler
2
Neato! encontró más información en la sección '3.4 Matriz de longitud variable' de programmersheaven.com/2/Pointers-and-Arrays-page-2
Arthur Ulfeldt
1
Pero eso no es diferente de manejar con punteros a variables locales. También pueden ser engañados con ...
glglgl
2
@Jonathan Leffler una cosa que puedes hacer con alloca pero que no puedes hacer con VLA es usar palabras clave restringidas con ellos. De esta manera: float * restrict heavy_used_arr = alloca (sizeof (float) * size); en lugar de float heavy_used_arr [tamaño]. Puede ayudar a algunos compiladores (gcc 4.8 en mi caso) a optimizar el ensamblaje incluso si el tamaño es una constante de compilación. Vea mi pregunta al respecto: stackoverflow.com/questions/19026643/using-restrict-with-arrays
Piotr Lopusiewicz
@JonathanLeffler Un VLA es local para el bloque que lo contiene. Por otro lado, alloca()asigna memoria que dura hasta el final de la función. Esto significa que no parece haber una traducción directa y conveniente al VLA de f() { char *p; if (c) { /* compute x */ p = alloca(x); } else { p = 0; } /* use p */ }. Si cree que es posible traducir automáticamente los usos de los allocausos de VLA pero requiere más que un comentario para describir cómo, puedo hacerle una pregunta.
Pascal Cuoq
40

Como se señaló en esta publicación de grupos de noticias , hay algunas razones por las cuales el uso allocapuede considerarse difícil y peligroso:

  • No todos los compiladores son compatibles alloca.
  • Algunos compiladores interpretan el comportamiento previsto de manera allocadiferente, por lo que la portabilidad no está garantizada incluso entre los compiladores que lo admiten.
  • Algunas implementaciones tienen errores.
Memoria libre
fuente
24
Una cosa que vi mencionada en ese enlace que no está en otra parte de esta página es que una función que utiliza alloca()requiere registros separados para mantener el puntero de pila y el puntero de marco. En las CPU x86> = 386, el puntero de la pila ESPse puede usar para ambos, liberando, a EBPmenos que alloca()se use.
j_random_hacker
10
Otro buen punto en esa página es que, a menos que el generador de código del compilador lo maneje como un caso especial, f(42, alloca(10), 43);podría bloquearse debido a la posibilidad de que el puntero de la pila se ajuste alloca() después de al menos uno de los argumentos.
j_random_hacker
3
La publicación vinculada parece estar escrita por John Levine, el tipo que escribió "Linkers and Loaders", confiaría en lo que él diga.
user318904
3
La publicación vinculada es una respuesta a una publicación de John Levine.
A. Wilcox
66
Tenga en cuenta que muchas cosas han cambiado desde 1991. Todos los compiladores de C modernos (incluso en 2009) tienen que manejar alloca como un caso especial; Es una función intrínseca más que ordinaria, y puede que ni siquiera llame a una función. Entonces, el problema de asignación de parámetros (que surgió en K&R C desde la década de 1970) no debería ser un problema ahora. Más detalles en un comentario que hice sobre la respuesta de Tony D
Greggo
26

Un problema es que no es estándar, aunque es ampliamente compatible. En igualdad de condiciones, siempre usaría una función estándar en lugar de una extensión de compilador común.

David Thornley
fuente
21

Todavía se desaconseja el uso de alloca, ¿por qué?

No percibo tal consenso. Muchos profesionales fuertes; algunas desventajas:

  • C99 proporciona matrices de longitud variable, que a menudo se utilizarían preferentemente como la notación más coherente con las matrices de longitud fija y en general intuitiva
  • muchos sistemas tienen menos espacio de memoria / dirección general disponible para la pila que para el montón, lo que hace que el programa sea un poco más susceptible al agotamiento de la memoria (a través del desbordamiento de la pila): esto puede verse como algo bueno o malo. Una de las razones por las que la pila no crece automáticamente como lo hace el montón es para evitar que los programas fuera de control tengan el mismo impacto adverso en toda la máquina
  • cuando se utiliza en un ámbito más local (como un bucle whileo for) o en varios ámbitos, la memoria se acumula por iteración / ámbito y no se libera hasta que la función finaliza: esto contrasta con las variables normales definidas en el ámbito de una estructura de control (p. ej. for {int i = 0; i < 2; ++i) { X }acumularía la allocamemoria -ed solicitada en X, pero la memoria para una matriz de tamaño fijo se reciclaría por iteración).
  • los compiladores modernos generalmente no inlinefuncionan esa llamada alloca, pero si los fuerza alloca, sucederá en el contexto de las personas que llaman (es decir, la pila no se lanzará hasta que la persona que llama regrese)
  • Hace mucho tiempo allocapasó de una función / pirateo no portátil a una extensión estandarizada, pero puede persistir cierta percepción negativa
  • la duración está vinculada al alcance de la función, que puede o no adaptarse mejor al programador que mallocel control explícito
  • tener que usar mallocalienta a pensar en la desasignación: si se administra a través de una función de contenedor (por ejemplo WonderfulObject_DestructorFree(ptr)), entonces la función proporciona un punto para las operaciones de limpieza de implementación (como cerrar descriptores de archivos, liberar punteros internos o hacer algunos registros) sin cambios explícitos en el cliente código: a veces es un buen modelo para adoptar consistentemente
    • en este estilo de programación pseudo-OO, es natural querer algo como WonderfulObject* p = WonderfulObject_AllocConstructor();eso, eso es posible cuando el "constructor" es una función que devuelve mallocmemoria (ya que la memoria permanece asignada después de que la función devuelve el valor que se almacenará p), pero no si el "constructor" usaalloca
      • una versión macro de WonderfulObject_AllocConstructorpodría lograr esto, pero las "macros son malvadas" en el sentido de que pueden entrar en conflicto entre sí y con código no macro y crear sustituciones no deseadas y los consecuentes problemas difíciles de diagnosticar
    • faltan freelas operaciones pueden ser detectados por valgrind, Purificar, etc, pero faltan las llamadas "Destructor" no siempre se pueden detectar en absoluto - un beneficio muy tenue en cuanto a la aplicación de uso previsto; algunas alloca()implementaciones (como las GCC) usan una macro en línea para alloca(), por lo que no es posible la sustitución en tiempo de ejecución de una biblioteca de diagnóstico de uso de memoria de la forma en que es para malloc/ realloc/ free(por ejemplo, cerca eléctrica)
  • Algunas implementaciones tienen problemas sutiles: por ejemplo, desde la página de manual de Linux:

    En muchos sistemas, alloca () no se puede usar dentro de la lista de argumentos de una llamada a función, porque el espacio de pila reservado por alloca () aparecería en la pila en el medio del espacio para los argumentos de función.


Sé que esta pregunta está etiquetada con C, pero como programador de C ++ pensé que usaría C ++ para ilustrar la utilidad potencial de alloca: el siguiente código (y aquí en ideone ) crea un vector que rastrea tipos polimórficos de diferentes tamaños que se asignan en pila (con vida útil vinculada al retorno de la función) en lugar del montón asignado.

#include <alloca.h>
#include <iostream>
#include <vector>

struct Base
{
    virtual ~Base() { }
    virtual int to_int() const = 0;
};

struct Integer : Base
{
    Integer(int n) : n_(n) { }
    int to_int() const { return n_; }
    int n_;
};

struct Double : Base
{
    Double(double n) : n_(n) { }
    int to_int() const { return -n_; }
    double n_;
};

inline Base* factory(double d) __attribute__((always_inline));

inline Base* factory(double d)
{
    if ((double)(int)d != d)
        return new (alloca(sizeof(Double))) Double(d);
    else
        return new (alloca(sizeof(Integer))) Integer(d);
}

int main()
{
    std::vector<Base*> numbers;
    numbers.push_back(factory(29.3));
    numbers.push_back(factory(29));
    numbers.push_back(factory(7.1));
    numbers.push_back(factory(2));
    numbers.push_back(factory(231.0));
    for (std::vector<Base*>::const_iterator i = numbers.begin();
         i != numbers.end(); ++i)
    {
        std::cout << *i << ' ' << (*i)->to_int() << '\n';
        (*i)->~Base();   // optionally / else Undefined Behaviour iff the
                         // program depends on side effects of destructor
    }
}
Tony Delroy
fuente
no +1 debido a la forma idiosincrásica a pescado de manejar varios tipos :-(
einpoklum
@einpoklum: bueno, eso es profundamente esclarecedor ... gracias.
Tony Delroy
1
Permítanme reformular: esta es una muy buena respuesta. Hasta el punto en que creo que estás sugiriendo que las personas usen una especie de contra-patrón.
einpoklum
El comentario de la página de manual de Linux es muy antiguo y, estoy bastante seguro, obsoleto. Todos los compiladores modernos saben qué es alloca (), y no tropezarán así con sus cordones. En el antiguo K&R C, (1) todas las funciones usaban punteros de trama (2) Todas las llamadas a funciones eran {push args en la pila} {call func} {add # n, sp}. alloca era una función lib que simplemente aumentaría la pila, el compilador ni siquiera sabía que eso estaba sucediendo. (1) y (2) ya no son ciertas, por lo que alloca no puede funcionar de esa manera (ahora es intrínseco). En la antigua C, llamar a alloca en medio de empujar args obviamente también rompería esas suposiciones.
Greggo
44
Con respecto al ejemplo, generalmente me preocuparía algo que requiere siempre_en línea para evitar la corrupción de la memoria ...
Greggo
14

Todas las otras respuestas son correctas. Sin embargo, si lo que desea asignar alloca()es razonablemente pequeño, creo que es una buena técnica que es más rápida y conveniente que usarla malloc()o no.

En otras palabras, alloca( 0x00ffffff )es peligroso y es probable que cause un desbordamiento, exactamente tanto como char hugeArray[ 0x00ffffff ];es. Sé prudente y razonable y estarás bien.

JSB ձոգչ
fuente
12

Muchas respuestas interesantes a esta "vieja" pregunta, incluso algunas respuestas relativamente nuevas, pero no encontré ninguna que mencione esto ...

Cuando se usa correctamente y con cuidado, el uso constante de alloca() (tal vez en toda la aplicación) para manejar pequeñas asignaciones de longitud variable (o VLA C99, donde esté disponible) puede conducir a un crecimiento de pila general más bajo que una implementación equivalente que usa matrices locales de gran tamaño de longitud fija . Por alloca()lo tanto, puede ser bueno para su pila si lo usa con cuidado.

Encontré esa cita en ... OK, hice esa cita. Pero realmente, piénsalo ...

@j_random_hacker tiene mucha razón en sus comentarios bajo otras respuestas: Evitar el uso alloca()a favor de matrices locales de gran tamaño no hace que su programa sea más seguro contra desbordamientos de pila (a menos que su compilador sea lo suficientemente viejo como para permitir la inclusión de funciones que se usan alloca()en ese caso, debería actualización, o a menos que use alloca()bucles internos, en cuyo caso no debería ... usar alloca()bucles internos).

He trabajado en entornos de escritorio / servidor y sistemas integrados. Muchos sistemas embebidos no usan un montón (ni siquiera se vinculan para soportarlo), por razones que incluyen la percepción de que la memoria asignada dinámicamente es mala debido a los riesgos de pérdidas de memoria en una aplicación que nunca siempre se reinicia durante años a la vez, o la justificación más razonable de que la memoria dinámica es peligrosa porque no se puede saber con certeza que una aplicación nunca fragmentará su montón hasta el punto de agotamiento de la memoria falsa. Por lo tanto, los programadores integrados tienen pocas alternativas.

alloca() (o VLA) puede ser la herramienta adecuada para el trabajo.

He visto una y otra vez que un programador crea un búfer asignado por pila "lo suficientemente grande como para manejar cualquier caso posible". En un árbol de llamadas profundamente anidado, el uso repetido de ese patrón (anti -?) Conduce a un uso exagerado de la pila. (Imagine un árbol de llamadas de 20 niveles de profundidad, donde en cada nivel por diferentes razones, la función asigna de forma ciega un búfer de 1024 bytes "solo para estar seguro" cuando generalmente solo usará 16 o menos de ellos, y solo en muy casos raros pueden usar más.) Una alternativa es usaralloca()o VLA y asigne solo la cantidad de espacio de pila que necesite su función, para evitar cargar innecesariamente la pila. Afortunadamente, cuando una función en el árbol de llamadas necesita una asignación más grande de lo normal, otras en el árbol de llamadas todavía usan sus pequeñas asignaciones normales, y el uso general de la pila de aplicaciones es significativamente menor que si cada función sobreasignara ciegamente un búfer local .

Pero si eliges usar alloca()...

Según otras respuestas en esta página, parece que los VLA deberían ser seguros (no combinan las asignaciones de pila si se invocan desde un bucle), pero si está usando alloca(), tenga cuidado de no usarlo dentro de un bucle y haga asegúrese de que su función no se pueda alinear si existe alguna posibilidad de que se invoque dentro del ciclo de otra función.

etiqueta de teléfono
fuente
Estoy de acuerdo con este punto. Lo peligroso de alloca()es cierto, pero lo mismo puede decirse de las pérdidas de memoria malloc()(¿por qué no usar un GC entonces? Uno podría discutir). alloca()cuando se usa con cuidado puede ser realmente útil para disminuir el tamaño de la pila.
Felipe Tonello
Otra buena razón para no usar memoria dinámica, especialmente en embebido: es más complicado que quedarse en la pila. El uso de memoria dinámica requiere procedimientos especiales y estructuras de datos, mientras que en la pila es (para simplificar las cosas) es cuestión de sumar / restar un número más alto de stackpointer.
tehftw
Nota al margen: El ejemplo de "usar un búfer fijo [MAX_SIZE]" resalta por qué la política de memoria de sobrecompromiso funciona tan bien. Los programas asignan memoria que nunca pueden tocar, excepto en los límites de su longitud de búfer. Entonces, está bien que Linux (y otros sistemas operativos) no asignen realmente una página de memoria hasta que se use por primera vez (en lugar de malloc'd). Si el búfer es más grande que una página, el programa solo puede usar la primera página y no desperdiciar el resto de la memoria física.
Katastic Voyage
@KatasticVoyage A menos que MAX_SIZE sea mayor (o al menos igual) al tamaño del tamaño de página de la memoria virtual de su sistema, su argumento no aguantará. También en sistemas integrados sin memoria virtual (muchas MCU integradas no tienen MMU), la política de memoria de sobrecompromiso puede ser buena desde el punto de vista de "asegurarse de que su programa se ejecutará en todas las situaciones", pero esa garantía viene con el precio de que su tamaño de pila también debe asignarse para admitir esa política de memoria de exceso de compromiso. En algunos sistemas integrados, ese es un precio que algunos fabricantes de productos de bajo costo no están dispuestos a pagar.
phonetagger
11

Todos ya han señalado lo importante que es el posible comportamiento indefinido de un desbordamiento de pila, pero debo mencionar que el entorno de Windows tiene un gran mecanismo para detectar esto utilizando excepciones estructuradas (SEH) y páginas de protección. Dado que la pila solo crece según sea necesario, estas páginas de protección residen en áreas que no están asignadas. Si los asigna a ellos (desbordando la pila), se genera una excepción.

Puede detectar esta excepción SEH y llamar a _resetstkoflw para restablecer la pila y continuar felizmente. No es ideal, pero es otro mecanismo para al menos saber que algo ha salido mal cuando las cosas golpean al ventilador. * nix podría tener algo similar de lo que no estoy al tanto.

Recomiendo limitar su tamaño máximo de asignación envolviendo alloca y rastreándolo internamente. Si usted fuera realmente duro al respecto, podría lanzar algunos centinelas de alcance en la parte superior de su función para rastrear cualquier asignación de asignación en el alcance de la función y verificar la sanidad contra la cantidad máxima permitida para su proyecto.

Además, además de no permitir pérdidas de memoria, alloca no causa fragmentación de memoria, lo cual es bastante importante. No creo que alloca sea una mala práctica si la usas de manera inteligente, lo cual es básicamente cierto para todo. :-)

Silencioso
fuente
El problema es que alloca()puede exigir tanto espacio que el stackpointer aterriza en el montón. Con eso, un atacante que puede controlar el tamaño alloca()y los datos que ingresan en ese búfer puede sobrescribir el montón (lo cual es muy malo).
12431234123412341234123
SEH es una cosa solo de Windows. Eso es genial si solo te importa que tu código se ejecute en Windows, pero si tu código necesita ser multiplataforma (o si estás escribiendo código que solo se ejecuta en una plataforma que no es Windows), entonces no puedes confiar en tener SEH
George
10

alloca () es agradable y eficiente ... pero también está profundamente roto.

  • comportamiento de alcance roto (alcance de función en lugar de alcance de bloque)
  • el uso inconsistente con malloc (el puntero asignado por alloca () no debe liberarse, de ahora en adelante debe rastrear de dónde provienen los punteros para liberar () solo aquellos que obtuvo con malloc () )
  • mal comportamiento cuando también usa inlining (el alcance a veces va a la función de la persona que llama dependiendo de si la persona que llama está en línea o no).
  • sin verificación de límite de pila
  • comportamiento indefinido en caso de falla (no devuelve NULL como malloc ... y qué significa falla ya que no verifica los límites de la pila de todos modos ...)
  • no estándar ansi

En la mayoría de los casos, puede reemplazarlo utilizando variables locales y tamaños mayores. Si se usa para objetos grandes, ponerlos en el montón suele ser una idea más segura.

Si realmente lo necesita C, puede usar VLA (sin vla en C ++, muy mal). Son mucho mejores que alloca () con respecto al comportamiento del alcance y la consistencia. Como lo veo, los VLA son un tipo de alloca () hecho a la derecha.

Por supuesto, una estructura local o matriz que utiliza un espacio importante del espacio necesario es aún mejor, y si no tiene una asignación de montón tan importante utilizando malloc simple () probablemente sea sensato. No veo ningún caso de uso sensato en el que realmente necesites alloca () o VLA.

kriss
fuente
No veo la razón del voto negativo (sin ningún comentario, por cierto)
gd1
Solo los nombres tienen alcance. allocano crea un nombre, solo un rango de memoria, que tiene vida útil .
curioso
@ curiousguy: simplemente estás jugando con palabras. Para las variables automáticas, también podría hablar de la vida útil de la memoria subyacente, ya que coincide con el alcance del nombre. De todos modos, el problema no es cómo lo llamamos, sino la inestabilidad de la vida útil / alcance de la memoria devuelta por alloca y el comportamiento excepcional.
kriss
2
Desearía que alloca hubiera tenido un "freea" correspondiente, con una especificación de que llamar a "freea" deshacería los efectos de la "alloca" que creó el objeto y todos los posteriores, y un requisito de que el almacenamiento 'alloca'ed dentro de una función debe ser 'liberado' dentro de él también. Eso habría hecho posible que casi todas las implementaciones admitieran alloca / freea de una manera compatible, hubiera aliviado los problemas de alineación y, en general, hubiera hecho las cosas mucho más limpias.
supercat
2
@supercat - Yo también lo deseo. Por eso (y más), utilizo una capa de abstracción (en su mayoría macros y funciones en línea) para que no me vuelvas a llamar allocao malloco freedirectamente. Digo cosas como {stack|heap}_alloc_{bytes,items,struct,varstruct}y {stack|heap}_dealloc. Entonces, heap_deallocsolo llama freey stack_dealloces un no-op. De esta manera, las asignaciones de la pila se pueden degradar fácilmente a asignaciones de montón, y las intenciones también son más claras.
Todd Lehman el
9

Este es el por qué:

char x;
char *y=malloc(1);
char *z=alloca(&x-y);
*z = 1;

No es que alguien escriba este código, pero el argumento de tamaño que está pasando allocacasi seguramente proviene de algún tipo de entrada, que podría apuntar maliciosamente a que su programa llegue a allocaalgo tan grande como eso. Después de todo, si el tamaño no se basa en la entrada o no tiene la posibilidad de ser grande, ¿por qué no declaraste un búfer local pequeño de tamaño fijo?

Prácticamente todo el código que usa allocay / o C99 vlas tiene errores graves que provocarán bloqueos (si tiene suerte) o un compromiso de privilegios (si no tiene tanta suerte).

R .. GitHub DEJA DE AYUDAR AL HIELO
fuente
1
El mundo tal vez nunca lo sepa. :( Dicho esto, espero que puedas aclarar una pregunta que tengo alloca. Dijiste que casi todo el código que lo usa tiene un error, pero estaba planeando usarlo; normalmente ignoraría tal afirmación, pero de ti no lo haré. Estoy escribiendo una máquina virtual y me gustaría asignar variables que no escapen de la función en la pila, en lugar de dinámicamente, debido a la enorme aceleración. ¿Hay una alternativa? enfoque que tiene las mismas características de rendimiento? Sé que puedo acercarme a los grupos de memoria, pero aún así no es tan barato. ¿Qué harías?
GManNickG
77
¿Sabes lo que también es peligroso? Esto: *0 = 9;¡INCREÍBLE! Supongo que nunca debería usar punteros (o al menos desreferenciarlos). Err, espera. Puedo probar para ver si es nulo. Hmm Supongo que también puedo probar el tamaño de la memoria que quiero asignar alloca. Hombre extraño. Extraño.
Thomas Eding
77
*0=9;no es válido C. En cuanto a probar el tamaño que pasa alloca, ¿probarlo con qué? No hay forma de conocer el límite, y si solo va a probarlo con un pequeño tamaño fijo seguro conocido (por ejemplo, 8k), también podría usar una matriz de tamaño fijo en la pila.
R .. GitHub DEJA DE AYUDAR A ICE
77
El problema con su argumento "se sabe que el tamaño es lo suficientemente pequeño o depende de la entrada y, por lo tanto, podría ser arbitrariamente grande", como veo, es que se aplica con la misma fuerza a la recursividad. Un compromiso práctico (para ambos casos) es asumir que si el tamaño está limitado small_constant * log(user_input)entonces probablemente tengamos suficiente memoria.
j_random_hacker
1
De hecho, ha identificado el caso ONE donde VLA / alloca es útil: algoritmos recursivos donde el espacio máximo necesario en cualquier marco de llamada podría ser tan grande como N, pero donde la suma del espacio necesario en todos los niveles de recursión es N o alguna función de N que no crece rápidamente.
R .. GitHub DEJA DE AYUDAR AL HIELO
9

No creo que nadie haya mencionado esto: el uso de alloca en una función obstaculizará o deshabilitará algunas optimizaciones que de otro modo podrían aplicarse en la función, ya que el compilador no puede conocer el tamaño del marco de la pila de la función.

Por ejemplo, una optimización común por parte de los compiladores de C es eliminar el uso del puntero de cuadro dentro de una función; en su lugar, los accesos de cuadro se hacen en relación con el puntero de pila; entonces hay un registro más para uso general. Pero si se llama a alloca dentro de la función, la diferencia entre sp y fp será desconocida para parte de la función, por lo que esta optimización no se puede hacer.

Dada la rareza de su uso, y su estado sombrío como función estándar, los diseñadores del compilador posiblemente deshabiliten cualquier optimización que pueda causar problemas con alloca, si tomaría más que un poco de esfuerzo para que funcione con alloca.

ACTUALIZACIÓN: Dado que se han agregado matrices locales de longitud variable a C, y dado que presentan problemas de generación de código muy similares para el compilador como alloca, veo que la 'rareza de uso y el estado sombreado' no se aplica al mecanismo subyacente; pero aún sospecharía que el uso de alloca o VLA tiende a comprometer la generación de código dentro de una función que los usa. Agradecería cualquier comentario de los diseñadores del compilador.

Greggo
fuente
1
Las matrices de longitud variable nunca se agregaron a C ++.
Nir Friedman el
@NirFriedman De hecho. Creo que hubo una lista de características de Wikipedia que se basó en una propuesta anterior.
greggo
> Todavía sospecharía que el uso de alloca o VLA tiende a comprometer la generación de código . Creo que el uso de alloca requiere un puntero de trama, porque el puntero de la pila se mueve de formas que no son obvias en el momento de la compilación. Se puede llamar a alloca en un bucle para seguir agarrando más memoria de pila, o con un tamaño calculado en tiempo de ejecución, etc. Si hay un puntero de cuadro, el código generado tiene una referencia estable a los locales y el puntero de pila puede hacer lo que quiera; No se utiliza.
Kaz
8

Un escollo con allocaeso es que lo longjmprebobina.

Es decir, si guarda un contexto con setjmp, a continuación, allocaalgo de memoria, a continuación, longjmpal contexto, es posible que pierda la allocamemoria. El puntero de la pila vuelve a estar donde estaba y, por lo tanto, la memoria ya no está reservada; si llama a una función o hace otra alloca, golpeará el original alloca.

Para aclarar, a lo que me refiero específicamente aquí es a una situación en la que longjmpno vuelve de la función donde allocatuvo lugar. Más bien, una función guarda contexto con setjmp; luego asigna memoria con allocay finalmente tiene lugar un longjmp en ese contexto. La allocamemoria de esa función no está completamente liberada; solo toda la memoria que asignó desde el setjmp. Por supuesto, estoy hablando de un comportamiento observado; no se documenta tal requisito de ninguno allocaque yo sepa.

El enfoque en la documentación generalmente está en el concepto de que la allocamemoria está asociada con la activación de una función , no con ningún bloque; que las invocaciones múltiples de allocasolo toman más memoria de pila que se libera cuando la función termina. No tan; la memoria está realmente asociada con el contexto del procedimiento. Cuando el contexto se restaura con longjmp, también lo es el allocaestado anterior . Es una consecuencia del registro del puntero de pila en sí mismo utilizado para la asignación, y también (necesariamente) guardado y restaurado en el jmp_buf.

Por cierto, esto, si funciona de esa manera, proporciona un mecanismo plausible para liberar deliberadamente la memoria que se le asignó alloca.

Me he encontrado con esto como la causa raíz de un error.

Kaz
fuente
1
Sin embargo, eso es lo que se supone que debe hacer: longjmpretrocede y lo hace para que el programa olvide todo lo que sucedió en la pila: todas las variables, llamadas a funciones, etc. Y allocaes como una matriz en la pila, por lo que se espera que sean golpeadas como todo lo demás en la pila.
tehftw
1
man allocadio la siguiente oración: "Debido a que el espacio asignado por alloca () se asigna dentro del marco de la pila, ese espacio se libera automáticamente si la función return se salta mediante una llamada a longjmp (3) o siglongjmp (3)". Por lo tanto, está documentado que la memoria asignada con allocase golpea después de a longjmp.
tehftw
@tehftw La situación descrita ocurre sin que se salte una función de retorno longjmp. La función de destino aún no ha regresado. Lo ha hecho setjmp, allocay luego longjmp. El longjmppuede rebobinar el allocaestado a lo que era en el setjmpmomento. Es decir, el puntero movido por allocasufre el mismo problema que una variable local que no ha sido marcada volatile.
Kaz
3
No entiendo por qué se supone que es inesperado. Cuando setjmpentonces alloca, y luego longjmp, es normal que allocase rebobine. ¡El objetivo de todo longjmpes volver al estado en el que se guardó setjmp!
tehftw
@tehftw Nunca he visto esta interacción particular documentada. Por lo tanto, no se puede confiar de ninguna manera, excepto por la investigación empírica con compiladores.
Kaz
7

Un lugar donde alloca()es especialmente peligroso que malloc()el núcleo: el núcleo de un sistema operativo típico tiene un espacio de pila de tamaño fijo codificado en uno de sus encabezados; No es tan flexible como la pila de una aplicación. Hacer una llamada a alloca()un tamaño injustificado puede hacer que el núcleo se bloquee. Ciertos compiladores advierten el uso de alloca()(e incluso VLA para ese caso) bajo ciertas opciones que deberían activarse al compilar un código de kernel; aquí, es mejor asignar memoria en el montón que no está fijada por un límite codificado.

Sondhi Chakraborty
fuente
77
alloca()no es más peligroso que int foo[bar];donde barhay algún número entero arbitrario.
Todd Lehman
@ToddLehman Eso es correcto, y por esa razón exacta hemos prohibido los VLA en el núcleo durante varios años, y hemos estado libres de VLA desde 2018 :-)
Chris Down
6

Si accidentalmente escribe más allá del bloque asignado con alloca(debido a un desbordamiento del búfer, por ejemplo), sobrescribirá la dirección de retorno de su función, porque esa se encuentra "arriba" en la pila, es decir, después de su bloque asignado.

_ bloque de asignación en la pila

La consecuencia de esto es doble:

  1. El programa se bloqueará espectacularmente y será imposible saber por qué o dónde se bloqueó (la pila probablemente se desenrollará en una dirección aleatoria debido al puntero de marco sobrescrito).

  2. Hace que el desbordamiento del búfer sea muchas veces más peligroso, ya que un usuario malintencionado puede crear una carga útil especial que se colocará en la pila y, por lo tanto, puede terminar ejecutándose.

Por el contrario, si escribe más allá de un bloque en el montón, "simplemente" se daña el montón. El programa probablemente terminará inesperadamente pero desenrollará la pila correctamente, reduciendo así la posibilidad de ejecución de código malicioso.

rustyx
fuente
11
Nada en esta situación es drásticamente diferente de los peligros de desbordar el búfer de un búfer asignado a la pila de tamaño fijo. Este peligro no es exclusivo de alloca.
phonetagger
2
Por supuesto no. Pero por favor revise la pregunta original. La pregunta es: ¿cuál es el peligro allocaen comparación con malloc(por lo tanto, no es un búfer de tamaño fijo en la pila).
rustyx 01 de
Punto menor, pero las pilas en algunos sistemas crecen hacia arriba (por ejemplo, microprocesadores PIC de 16 bits).
EBlake
5

Lamentablemente, lo verdaderamente increíble alloca()falta en el tcc casi increíble. Gcc tiene alloca().

  1. Siembra la semilla de su propia destrucción. Con retorno como el destructor.

  2. Al igual malloc()que devuelve un puntero no válido en caso de error que segfault en los sistemas modernos con una MMU (y con suerte reiniciará los que no lo tengan).

  3. A diferencia de las variables automáticas, puede especificar el tamaño en tiempo de ejecución.

Funciona bien con recursividad. Puede usar variables estáticas para lograr algo similar a la recursividad de cola y usar solo algunas otras para pasar información a cada iteración.

Si presiona demasiado, tiene la seguridad de una falla predeterminada (si tiene una MMU).

Tenga en cuenta que malloc()no ofrece más, ya que devuelve NULL (que también será predeterminado si se asigna) cuando el sistema no tiene memoria. Es decir, todo lo que puede hacer es rescatar o simplemente tratar de asignarlo de cualquier manera.

Para usarlo malloc()uso globales y les asigno NULL. Si el puntero no es NULL, lo libero antes de usarlo malloc().

También puede usar realloc()como caso general si desea copiar cualquier dato existente. Debe comprobar el puntero antes de determinar si va a copiar o concatenar después de realloc().

3.2.5.2 Ventajas de alloca

zagam
fuente
44
En realidad, la especificación alloca no dice que devuelve un puntero no válido en caso de error (desbordamiento de pila), dice que tiene un comportamiento indefinido ... y para malloc dice que devuelve NULL, no un puntero aleatorio no válido (OK, la implementación de memoria optimista de Linux hace que inútil).
kriss
@kriss Linux puede matar su proceso, pero al menos no se aventura en un comportamiento indefinido
craig65535
@ craig65535: la expresión comportamiento indefinido generalmente significa que ese comportamiento no está definido por la especificación C o C ++. De ninguna manera será aleatorio o inestable en cualquier sistema operativo o compilador. Por lo tanto, no tiene sentido asociar UB con el nombre de un sistema operativo como "Linux" o "Windows". No tiene nada que ver con ello.
kriss
Estaba tratando de decir que malloc devolviendo NULL, o en el caso de Linux, un acceso a la memoria que mata su proceso, es preferible al comportamiento indefinido de alloca. Creo que debí haber leído mal tu primer comentario.
craig65535
3

Los procesos solo tienen una cantidad limitada de espacio de pila disponible, mucho menos que la cantidad de memoria disponible malloc().

Al usarlo alloca(), aumenta dramáticamente sus posibilidades de obtener un error de desbordamiento de pila (si tiene suerte, o un bloqueo inexplicable si no lo tiene)

RichieHindle
fuente
Eso depende mucho de la aplicación. No es inusual que una aplicación incrustada con memoria limitada tenga un tamaño de pila más grande que el montón (si incluso hay un montón).
EBlake
3

No es muy bonito, pero si el rendimiento realmente importa, puede preasignar algo de espacio en la pila.

Si ya tiene el tamaño máximo de la memoria que bloquea su necesidad y desea mantener las comprobaciones de desbordamiento, puede hacer algo como:

void f()
{
    char array_on_stack[ MAX_BYTES_TO_ALLOCATE ];
    SomeType *p = (SomeType *)array;

    (...)
}
Sylvain Rodrigue
fuente
12
¿Se garantiza que la matriz de caracteres esté correctamente alineada para cualquier tipo de datos? alloca ofrece tal promesa.
Juho Östman
@ JuhoÖstman: puede usar una matriz de struct (o de cualquier tipo) en lugar de char si tiene problemas de alineación.
kriss
Eso se llama una matriz de longitud variable . Se admite en C90 y superior, pero no en C ++. Consulte ¿Puedo usar una matriz de longitud variable de C en C ++ 03 y C ++ 11?
jww
3

La función alloca es excelente y todos los detractores simplemente están difundiendo FUD.

void foo()
{
    int x = 50000; 
    char array[x];
    char *parray = (char *)alloca(x);
}

Array y parray son EXACTAMENTE iguales con EXACTAMENTE los mismos riesgos. Decir que uno es mejor que otro es una opción sintáctica, no técnica.

En cuanto a la elección de las variables de pila frente a las variables de montón, hay MUCHAS ventajas para los programas de ejecución prolongada que usan pila sobre montón para variables con vidas dentro del alcance. Evita la fragmentación del montón y puede evitar aumentar su espacio de proceso con espacio de montón no utilizado (inutilizable). No necesitas limpiarlo. Puede controlar la asignación de la pila en el proceso.

¿Por qué es esto malo?

mlwmohawk
fuente
3

En realidad, no se garantiza que alloca use la pila. De hecho, la implementación gcc-2.95 de alloca asigna memoria del montón usando el propio malloc. Además, esa implementación tiene errores, puede provocar una pérdida de memoria y un comportamiento inesperado si lo llama dentro de un bloque con un uso adicional de goto. No, para decir que nunca deberías usarlo, pero algunas veces alloca conduce a más sobrecarga de la que libera de ti.

usuario7491277
fuente
Parece que gcc-2.95 rompió alloca y probablemente no se puede usar de manera segura para los programas que lo requieren alloca. ¿Cómo habría limpiado la memoria cuando longjmpse usa para abandonar marcos que lo hicieron alloca? ¿Cuándo alguien usaría gcc 2.95 hoy?
Kaz
2

En mi humilde opinión, alloca se considera una mala práctica porque todo el mundo tiene miedo de agotar el límite de tamaño de la pila.

Aprendí mucho leyendo este hilo y algunos otros enlaces:

Uso alloca principalmente para hacer que mis archivos C simples sean compilables en msvc y gcc sin ningún cambio, estilo C89, sin #ifdef _MSC_VER, etc.

Gracias ! Este hilo me hizo registrarme en este sitio :)

Ytoto
fuente
Tenga en cuenta que no hay tal cosa como un "hilo" en este sitio. Stack Overflow tiene un formato de preguntas y respuestas, no un formato de hilo de discusión. "Responder" no es como "Responder" en un foro de discusión; significa que en realidad está proporcionando una respuesta a la pregunta, y no debe usarse para responder a otras respuestas o comentar sobre el tema. Una vez que tenga al menos 50 representantes, puede publicar comentarios , pero asegúrese de leer el "¿Cuándo no debo comentar?" sección. Lea la página Acerca de para comprender mejor el formato del sitio.
Adi Inbar
1

En mi opinión, alloca (), donde esté disponible, debe usarse solo de manera restringida. Al igual que el uso de "goto", un gran número de personas razonables tienen una fuerte aversión no solo por el uso, sino también por la existencia de alloca ().

Para uso incrustado, donde se conoce el tamaño de la pila y se pueden imponer límites por convención y análisis sobre el tamaño de la asignación, y donde el compilador no se puede actualizar para admitir C99 +, el uso de alloca () está bien, y he estado conocido por usarlo.

Cuando esté disponible, los VLA pueden tener algunas ventajas sobre alloca (): el compilador puede generar comprobaciones de límite de pila que capturarán el acceso fuera de los límites cuando se usa el acceso de estilo de matriz (no sé si algún compilador hace esto, pero puede hacer), y el análisis del código puede determinar si las expresiones de acceso a la matriz están correctamente delimitadas. Tenga en cuenta que, en algunos entornos de programación, como automoción, equipos médicos y aviónica, este análisis debe realizarse incluso para matrices de tamaño fijo, tanto automáticas (en la pila) como de asignación estática (global o local).

En las arquitecturas que almacenan datos y direcciones de retorno / punteros de trama en la pila (por lo que sé, eso es todo), cualquier variable asignada a la pila puede ser peligrosa porque la dirección de la variable puede tomarse y los valores de entrada no verificados pueden permitir todo tipo de travesuras.

La portabilidad es menos preocupante en el espacio incrustado, sin embargo, es un buen argumento contra el uso de alloca () fuera de circunstancias cuidadosamente controladas.

Fuera del espacio incrustado, he usado alloca () principalmente dentro de las funciones de registro y formateo para mayor eficiencia, y en un escáner léxico no recursivo, donde las estructuras temporales (asignadas usando alloca () se crean durante la tokenización y clasificación, luego persiste object (asignado a través de malloc ()) se rellena antes de que regrese la función. El uso de alloca () para las estructuras temporales más pequeñas reduce en gran medida la fragmentación cuando se asigna el objeto persistente.

Daniel Glasser
fuente
1

La mayoría de las respuestas aquí en gran medida pierden el punto: hay una razón por la cual usar _alloca() es potencialmente peor que simplemente almacenar objetos grandes en la pila.

La principal diferencia entre el almacenamiento automático y _alloca() el último es que tiene un problema adicional (grave): el compilador no controla el bloque asignado, por lo que el compilador no puede optimizarlo ni reciclarlo.

Comparar:

while (condition) {
    char buffer[0x100]; // Chill.
    /* ... */
}

con:

while (condition) {
    char* buffer = _alloca(0x100); // Bad!
    /* ... */
}

El problema con este último debería ser obvio.

alecov
fuente
¿Tiene algún ejemplo práctico que demuestre la diferencia entre VLA y alloca(sí, digo VLA, porque allocaes más que un simple creador de matrices de tamaño estático)?
Ruslan
Hay casos de uso para el segundo, que el primero no admite. Es posible que desee tener registros 'n' después de que el ciclo termine de ejecutarse 'n' veces, tal vez en una lista vinculada o en un árbol; Esta estructura de datos se elimina cuando la función finalmente regresa. Lo que no quiere decir que codificaría cualquier cosa de esa manera :-)
greggo
1
Y diría que "el compilador no puede controlarlo" es porque esa es la forma en que se define alloca (); los compiladores modernos saben qué es alloca y lo tratan especialmente; no es solo una función de biblioteca como era en los años 80. Los VLA C99 son básicamente alloca con alcance de bloque (y una mejor tipificación). No hay más o menos control, solo se ajusta a diferentes semánticas.
greggo
@greggo: Si eres el votante negativo, con gusto escucharé por qué piensas que mi respuesta no es útil.
alecov
En C, el reciclaje no es tarea del compilador, sino de la biblioteca c (free ()). alloca () se libera al regresar.
peterh - Restablece a Mónica el
1

No creo que nadie haya mencionado esto, pero alloca también tiene algunos problemas de seguridad graves que no necesariamente están presentes con malloc (aunque estos problemas también surgen con cualquier matriz basada en pila, dinámica o no). Dado que la memoria está asignada en la pila, los desbordamientos / desbordamientos del búfer tienen consecuencias mucho más graves que con solo malloc.

En particular, la dirección de retorno de una función se almacena en la pila. Si este valor se corrompe, se puede hacer que su código vaya a cualquier región ejecutable de la memoria. Los compiladores hacen todo lo posible para dificultar esto (en particular al aleatorizar el diseño de la dirección). Sin embargo, esto es claramente peor que un desbordamiento de pila, ya que el mejor de los casos es un SEGFAULT si el valor de retorno está dañado, pero también podría comenzar a ejecutar una memoria aleatoria o, en el peor de los casos, alguna región de memoria que comprometa la seguridad de su programa. .

Dyllon Gagnier
fuente