Orden de asignación de variable local en la stack

Eche un vistazo a estas dos funciones:

void function1() { int x; int y; int z; int *ret; } void function2() { char buffer1[4]; char buffer2[4]; char buffer3[4]; int *ret; } 

Si rompo en function1() en gdb , e imprimo las direcciones de las variables, obtengo esto:

 (gdb) p &x $1 = (int *) 0xbffff380 (gdb) p &y $2 = (int *) 0xbffff384 (gdb) p &z $3 = (int *) 0xbffff388 (gdb) p &ret $4 = (int **) 0xbffff38c 

Si hago lo mismo en function2() , obtengo esto:

 (gdb) p &buffer1 $1 = (char (*)[4]) 0xbffff388 (gdb) p &buffer2 $2 = (char (*)[4]) 0xbffff384 (gdb) p &buffer3 $3 = (char (*)[4]) 0xbffff380 (gdb) p &ret $4 = (int **) 0xbffff38c 

Notarás que en ambas funciones, ret se almacena más cerca de la parte superior de la stack. En function1() , es seguido por z , y , y finalmente x . En function2() , ret es seguido por buffer1 , luego buffer2 y buffer3 . ¿Por qué se cambia la orden de almacenamiento? Estamos usando la misma cantidad de memoria en ambos casos (matrices de 4 bytes frente a matrices de 4 bytes), por lo que no puede ser un problema de relleno. ¿Qué razones podría haber para este reordenamiento, y además, es posible mirando el código C para determinar de antemano cómo se ordenarán las variables locales?

Ahora soy consciente de que las especificaciones de ANSI para C no dicen nada sobre el orden en que se almacenan las variables locales y que el comstackdor puede elegir su propio orden, pero me imagino que el comstackdor tiene reglas sobre cómo se ocupa de las mismas. esto, y explicaciones de por qué esas reglas fueron hechas para ser como son.

Como referencia, estoy usando GCC 4.0.1 en Mac OS 10.5.7

No tengo idea de por qué GCC organiza su stack de la forma en que lo hace (aunque supongo que podrías descifrar su fuente o este documento y averiguarlo), pero puedo decirte cómo garantizar el orden de las variables de stack específicas si por alguna razón necesitas. Simplemente ponlos en una estructura:

 void function1() { struct { int x; int y; int z; int *ret; } locals; } 

Si mi memoria me sirve correctamente, spec garantiza que &ret > &z > &y > &x . Dejé mi K & R en el trabajo, así que no puedo citar el capítulo y el versículo.

Entonces, hice algo más de experimentación y esto es lo que encontré. Parece estar basado en si cada variable es o no una matriz. Teniendo en cuenta esta entrada:

 void f5() { int w; int x[1]; int *ret; int y; int z[1]; } 

Termino con esto en gdb:

 (gdb) p &w $1 = (int *) 0xbffff4c4 (gdb) p &x $2 = (int (*)[1]) 0xbffff4c0 (gdb) p &ret $3 = (int **) 0xbffff4c8 (gdb) p &y $4 = (int *) 0xbffff4cc (gdb) p &z $5 = (int (*)[1]) 0xbffff4bc 

En este caso, los int y los punteros se tratan primero, el último declarado en la parte superior de la stack y el primero declarado más cerca del final. Luego, las matrices se manejan, en la dirección opuesta, cuanto antes la statement, más arriba en la stack. Estoy seguro de que hay una buena razón para esto. Me pregunto qué es.

El ISO C no solo no dice nada sobre el orden de las variables locales en la stack, ni siquiera garantiza que exista una stack. El estándar solo habla sobre el scope y la duración de las variables dentro de un bloque.

Por lo general, tiene que ver con problemas de alineación.

La mayoría de los procesadores son más lentos en la búsqueda de datos que no están alineados con la palabra del procesador. Deben agarrarlo en pedazos y empalmarlo.

Probablemente lo que está sucediendo es que está juntando todos los objetos que son mayores o iguales a la alineación óptima del procesador, y luego ajustando más apretadamente las cosas que pueden no estar alineadas. Sucede que en tu ejemplo todas tus matrices de caracteres son de 4 bytes, pero apuesto a que si los conviertes en 3 bytes, terminarán en los mismos lugares.

Pero si tiene cuatro matrices de un byte, pueden terminar en un rango de 4 bytes, o alinearse en cuatro separadas.

Se trata de lo que es más fácil (se traduce en “más rápido”) para que el procesador lo agarre.

El estándar C no dicta ningún diseño para las otras variables automáticas. Sin embargo, específicamente dice, para evitar dudas, que

[…] El diseño del almacenamiento para los parámetros [función] no está especificado. (C11 6.9.1p9)

Se puede entender por eso que el diseño de almacenamiento para cualquier otro objeto tampoco se especifica, excepto para los pocos requisitos que da el estándar, incluyendo que el puntero nulo no puede apuntar a ningún objeto o función válida, y los diseños dentro del agregado objetos.

El estándar C no contiene una sola mención a la palabra “stack”; es muy posible hacer, por ejemplo, una implementación de C sin astackmiento, asignando cada registro de activación del montón (aunque tal vez se podría entender entonces que forman una stack).

Una de las razones para dar al comstackdor cierto margen de maniobra es la eficiencia. Sin embargo, los comstackdores actuales también usarían esto para la seguridad, utilizando trucos como la asignación al azar del diseño del espacio de direcciones y la stack de canarios para tratar de hacer más difícil la explotación del comportamiento indefinido . El reordenamiento de los buffers se hace para que el uso de canary sea más efectivo.

Supongo que esto tiene algo que ver con la forma en que se cargan los datos en los registros. Quizás, con las matrices de caracteres, el comstackdor funciona algo de magia para hacer cosas en paralelo y esto tiene algo que ver con la posición en la memoria para cargar fácilmente los datos en los registros. Intente comstackr con diferentes niveles de optimización e intente usar int buffer1[1] lugar.

¿También podría ser un problema de seguridad?

 int main() { int array[10]; int i; for (i = 0; i <= 10; ++i) { array[i] = 0; } } 

Si la matriz es más baja en la stack que yo, este código se repetirá infinitamente (porque tiene acceso erróneo y pone a cero la matriz [10], que es i). Al colocar la matriz más arriba en la stack, los bashs de acceder a la memoria más allá del final de la stack tendrán más probabilidades de tocar la memoria no asignada y colgarse, en lugar de causar un comportamiento indefinido.

Experimenté con este mismo código una vez con gcc, y no pude hacerlo fallar excepto con una combinación particular de banderas que ahora no recuerdo ... En cualquier caso, colocó la matriz varios bytes de distancia de i.

Curiosamente, si agrega un int * ret2 extra en función1, entonces el orden es correcto, mientras que está fuera de servicio solo para 3 variables locales. Supongo que está ordenado de esa manera debido a que refleja la estrategia de asignación de registros que se usará. O eso o es arbitrario.

Todo depende del comstackdor. Más allá de esto, ciertas variables de procedimiento podrían nunca colocarse en la stack, ya que pueden pasar toda su vida dentro de un registro.